From e99e4353d48cecc9c780f6bb5d45dea1401fc7d6 Mon Sep 17 00:00:00 2001 From: Gaurav Thakur Date: Tue, 30 Jul 2019 20:04:04 +0530 Subject: Remove Google+ support as it is dead. (#2464) --- cli/i18n/ignore/en.php | 2 -- cli/i18n/ignore/fr.php | 2 -- cli/i18n/ignore/kr.php | 2 -- cli/i18n/ignore/nl.php | 2 -- cli/i18n/ignore/oc.php | 2 -- cli/i18n/ignore/zh-cn.php | 2 -- 6 files changed, 12 deletions(-) (limited to 'cli') diff --git a/cli/i18n/ignore/en.php b/cli/i18n/ignore/en.php index e231afdda..d1ab44b60 100644 --- a/cli/i18n/ignore/en.php +++ b/cli/i18n/ignore/en.php @@ -67,7 +67,6 @@ return array( 'conf.sharing.diaspora', 'conf.sharing.email', 'conf.sharing.facebook', - 'conf.sharing.g+', 'conf.sharing.print', 'conf.sharing.shaarli', 'conf.sharing.twitter', @@ -88,7 +87,6 @@ return array( 'gen.share.diaspora', 'gen.share.email', 'gen.share.facebook', - 'gen.share.g+', 'gen.share.movim', 'gen.share.print', 'gen.share.shaarli', diff --git a/cli/i18n/ignore/fr.php b/cli/i18n/ignore/fr.php index 0ac2e8758..16528d28c 100644 --- a/cli/i18n/ignore/fr.php +++ b/cli/i18n/ignore/fr.php @@ -8,7 +8,6 @@ return array( 'conf.sharing.blogotext', 'conf.sharing.diaspora', 'conf.sharing.facebook', - 'conf.sharing.g+', 'conf.sharing.print', 'conf.sharing.shaarli', 'conf.sharing.twitter', @@ -35,7 +34,6 @@ return array( 'gen.share.blogotext', 'gen.share.diaspora', 'gen.share.facebook', - 'gen.share.g+', 'gen.share.movim', 'gen.share.shaarli', 'gen.share.twitter', diff --git a/cli/i18n/ignore/kr.php b/cli/i18n/ignore/kr.php index bbccb2d7d..3c4ea84b8 100644 --- a/cli/i18n/ignore/kr.php +++ b/cli/i18n/ignore/kr.php @@ -4,7 +4,6 @@ return array( 'conf.sharing.blogotext', 'conf.sharing.diaspora', 'conf.sharing.facebook', - 'conf.sharing.g+', 'conf.sharing.shaarli', 'conf.sharing.twitter', 'conf.sharing.wallabag', @@ -39,7 +38,6 @@ return array( 'gen.share.blogotext', 'gen.share.diaspora', 'gen.share.facebook', - 'gen.share.g+', 'gen.share.gnusocial', 'gen.share.jdh', 'gen.share.linkedin', diff --git a/cli/i18n/ignore/nl.php b/cli/i18n/ignore/nl.php index 4013bc89e..6d28d68ed 100644 --- a/cli/i18n/ignore/nl.php +++ b/cli/i18n/ignore/nl.php @@ -5,7 +5,6 @@ return array( 'conf.sharing.diaspora', 'conf.sharing.email', 'conf.sharing.facebook', - 'conf.sharing.g+', 'conf.sharing.print', 'conf.sharing.shaarli', 'conf.sharing.twitter', @@ -31,7 +30,6 @@ return array( 'gen.share.diaspora', 'gen.share.facebook', 'gen.share.email', - 'gen.share.g+', 'gen.share.mastodon', 'gen.share.movim', 'gen.share.print', diff --git a/cli/i18n/ignore/oc.php b/cli/i18n/ignore/oc.php index 6413fc5f0..04a4ad68e 100644 --- a/cli/i18n/ignore/oc.php +++ b/cli/i18n/ignore/oc.php @@ -8,7 +8,6 @@ return array( 'conf.sharing.blogotext', 'conf.sharing.diaspora', 'conf.sharing.facebook', - 'conf.sharing.g+', 'conf.sharing.print', 'conf.sharing.shaarli', 'conf.sharing.twitter', @@ -36,7 +35,6 @@ return array( 'gen.share.blogotext', 'gen.share.diaspora', 'gen.share.facebook', - 'gen.share.g+', 'gen.share.movim', 'gen.share.shaarli', 'gen.share.twitter', diff --git a/cli/i18n/ignore/zh-cn.php b/cli/i18n/ignore/zh-cn.php index d55071d10..a1468a0b1 100644 --- a/cli/i18n/ignore/zh-cn.php +++ b/cli/i18n/ignore/zh-cn.php @@ -5,7 +5,6 @@ return array( 'conf.sharing.diaspora', 'conf.sharing.email', 'conf.sharing.facebook', - 'conf.sharing.g+', 'conf.sharing.shaarli', 'conf.sharing.twitter', 'conf.sharing.wallabag', @@ -27,7 +26,6 @@ return array( 'gen.share.diaspora', 'gen.share.email', 'gen.share.facebook', - 'gen.share.g+', 'gen.share.gnusocial', 'gen.share.jdh', 'gen.share.linkedin', -- cgit v1.2.3 From f5fbc0c7f08efd616a4d24985916d88a846d1723 Mon Sep 17 00:00:00 2001 From: koocotte Date: Wed, 31 Jul 2019 13:48:06 +0200 Subject: Patch for #2460: Run on Apache 2.4+ without mod_access_compat (#2461) * Update .htaccess * Update htaccess for apache2.4 * Update htaccess for apache2.4 * Update htaccess for apache2.4 --- app/.htaccess | 14 +++++++++++--- cli/.htaccess | 14 +++++++++++--- data/.htaccess | 14 +++++++++++--- lib/.htaccess | 14 +++++++++++--- 4 files changed, 44 insertions(+), 12 deletions(-) (limited to 'cli') diff --git a/app/.htaccess b/app/.htaccess index 9e768397d..32eca30f7 100644 --- a/app/.htaccess +++ b/app/.htaccess @@ -1,3 +1,11 @@ -Order Allow,Deny -Deny from all -Satisfy all +# Apache 2.2 + + Order Allow,Deny + Deny from all + Satisfy all + + +# Apache 2.4 + + Require all denied + diff --git a/cli/.htaccess b/cli/.htaccess index 9e768397d..32eca30f7 100644 --- a/cli/.htaccess +++ b/cli/.htaccess @@ -1,3 +1,11 @@ -Order Allow,Deny -Deny from all -Satisfy all +# Apache 2.2 + + Order Allow,Deny + Deny from all + Satisfy all + + +# Apache 2.4 + + Require all denied + diff --git a/data/.htaccess b/data/.htaccess index 9e768397d..32eca30f7 100644 --- a/data/.htaccess +++ b/data/.htaccess @@ -1,3 +1,11 @@ -Order Allow,Deny -Deny from all -Satisfy all +# Apache 2.2 + + Order Allow,Deny + Deny from all + Satisfy all + + +# Apache 2.4 + + Require all denied + diff --git a/lib/.htaccess b/lib/.htaccess index 9e768397d..32eca30f7 100644 --- a/lib/.htaccess +++ b/lib/.htaccess @@ -1,3 +1,11 @@ -Order Allow,Deny -Deny from all -Satisfy all +# Apache 2.2 + + Order Allow,Deny + Deny from all + Satisfy all + + +# Apache 2.4 + + Require all denied + -- cgit v1.2.3 From c82aff177e53685c66d1bd1ab41a893bef29caef Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 14 Aug 2019 15:25:28 +0200 Subject: fix: Generate correct htaccess in cli/prepare (#2480) Bug introduced in https://github.com/FreshRSS/FreshRSS/pull/2461 --- cli/prepare.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'cli') diff --git a/cli/prepare.php b/cli/prepare.php index 81fb53f85..7e8ea051d 100755 --- a/cli/prepare.php +++ b/cli/prepare.php @@ -28,9 +28,17 @@ if (!is_file(DATA_PATH . '/config.php')) { } file_put_contents(DATA_PATH . '/.htaccess', -"Order Allow,Deny\n" . -"Deny from all\n" . -"Satisfy all\n" +"# Apache 2.2\n" . +"\n" . +" Order Allow,Deny\n" . +" Deny from all\n" . +" Satisfy all\n" . +"\n" . +"\n" . +"# Apache 2.4\n" . +"\n" . +" Require all denied\n" . +"\n" ); accessRights(); -- cgit v1.2.3 From 520676893fa9fc14dfe4e611a70b1ab305d5c222 Mon Sep 17 00:00:00 2001 From: Tibor Repček Date: Tue, 20 Aug 2019 21:20:57 +0200 Subject: [i18n] Slovak (slovenčina) - added new language (#2497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/i18n/sk/admin.php | 199 +++++++++++++++++++++++++++++++++++++++++++++++ app/i18n/sk/conf.php | 188 ++++++++++++++++++++++++++++++++++++++++++++ app/i18n/sk/feedback.php | 116 +++++++++++++++++++++++++++ app/i18n/sk/gen.php | 197 ++++++++++++++++++++++++++++++++++++++++++++++ app/i18n/sk/index.php | 63 +++++++++++++++ app/i18n/sk/install.php | 123 +++++++++++++++++++++++++++++ app/i18n/sk/sub.php | 100 ++++++++++++++++++++++++ cli/i18n/ignore/sk.php | 66 ++++++++++++++++ 8 files changed, 1052 insertions(+) create mode 100644 app/i18n/sk/admin.php create mode 100644 app/i18n/sk/conf.php create mode 100644 app/i18n/sk/feedback.php create mode 100644 app/i18n/sk/gen.php create mode 100644 app/i18n/sk/index.php create mode 100644 app/i18n/sk/install.php create mode 100644 app/i18n/sk/sub.php create mode 100644 cli/i18n/ignore/sk.php (limited to 'cli') diff --git a/app/i18n/sk/admin.php b/app/i18n/sk/admin.php new file mode 100644 index 000000000..347204f37 --- /dev/null +++ b/app/i18n/sk/admin.php @@ -0,0 +1,199 @@ + array( + 'allow_anonymous' => 'Povoliť čítanie článkov prednastaveného používateľa (%s) bez prihlásenia.', + 'allow_anonymous_refresh' => 'Povoliť obnovenie článkov bez prihlásenia', + 'api_enabled' => 'Povoliť prístup cez API (vyžadujú mobilné aplikácie)', + 'form' => 'Webový formulár (traditičný, vyžaduje JavaScript)', + 'http' => 'HTTP (pre pokročilých používateľov s HTTPS)', + 'none' => 'Žiadny (nebezpečné)', + 'title' => 'Prihlásenie', + 'title_reset' => 'Reset prihlásenia', + 'token' => 'Token prihlásenia', + 'token_help' => 'Povoliť prístup k výstupu RSS prednastaveného používateľa bez prihlásenia:', + 'type' => 'Spôsob prihlásenia', + 'unsafe_autologin' => 'Povoliť nebezpečné automatické prihlásenie pomocou webového formulára: ', + ), + 'check_install' => array( + 'cache' => array( + 'nok' => 'Overte prístupové práva priečinka ./data/cache. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Prístupové práva priečinka pre vyrovnávaciu pamäť sú OK.', + ), + 'categories' => array( + 'nok' => 'Tabuľka kategórií je nesprávne nastavená.', + 'ok' => 'Tabuľka kategórií je OK.', + ), + 'connection' => array( + 'nok' => 'Nepodarilo sa vytvoriť pripojenie k databáze.', + 'ok' => 'Pripojenie k databáze je OK.', + ), + 'ctype' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na kontrolu typu znakov (php-ctype).', + 'ok' => 'Našla sa požadovaná knižnica na kontrolu typu znakov (ctype).', + ), + 'curl' => array( + 'nok' => 'Nepodarilo sa nájsť knižnicu cURL (balík php-curl).', + 'ok' => 'Našla sa knižnica cURL.', + ), + 'data' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku údajov sú OK.', + ), + 'database' => 'Inštalácia databázy', + 'dom' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na prehliadanie DOM.', + 'ok' => 'Našla sa požadovaná knižnica na prehliadanie DOM.', + ), + 'entries' => array( + 'nok' => 'Tabuľka článkov je nesprávne nastavená.', + 'ok' => 'Tabuľka článkov je OK.', + ), + 'favicons' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data/favicons. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku ikôn obľúbených sú OK.', + ), + 'feeds' => array( + 'nok' => 'Tabuľka kanálov je nesprávne nastavená.', + 'ok' => 'Tabuľka kanálov je OK.', + ), + 'fileinfo' => array( + 'nok' => 'Nepodarilo sa nájsť knižniuc PHP fileinfo (balík fileinfo).', + 'ok' => 'Našla sa knižnica fileinfo.', + ), + 'files' => 'Inštalácia súborov', + 'json' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na spracovanie formátu JSON.', + 'ok' => 'Našla sa požadovaná knižnica na spracovanie formátu JSON.', + ), + 'mbstring' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu mbstring pre Unicode.', + 'ok' => 'Našla sa požadovaná knižnica mbstring pre Unicode.', + ), + 'minz' => array( + 'nok' => 'Nepodarilo sa nájsť framework Minz.', + 'ok' => 'Našiel sa framework Minz.', + ), + 'pcre' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu pre regulárne výrazy (php-pcre).', + 'ok' => 'Našla sa požadovaná knižnica pre regulárne výrazy (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Nepodarilo sa nájsť PDO alebo niektorý z podporovaných ovládačov (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Našiel sa PDO a aspoň jeden z podporovaných ovládačov (pdo_mysql, pdo_sqlite, pdo_pgsql).', + ), + 'php' => array( + '_' => 'Inštalácia PHP', + 'nok' => 'Vaša verzia PHP je %s, ale FreshRSS vyžaduje minimálne verziu %s.', + 'ok' => 'Vaša verzia PHP %s je kompatibilná s FreshRSS.', + ), + 'tables' => array( + 'nok' => 'V databáze chýba jedna alebo viacero tabuliek.', + 'ok' => 'V databáze sa nachádzajú všetky potrebné tabuľky.', + ), + 'title' => 'Kontrola inštalácie', + 'tokens' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data/tokens. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku tokens sú OK.', + ), + 'users' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data/users. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku používateľov sú OK.', + ), + 'zip' => array( + 'nok' => 'Nepodarilo sa nájsť rozšírenie ZIP (balík php-zip).', + 'ok' => 'Rozšírenie ZIP sa našlo.', + ), + ), + 'extensions' => array( + 'author' => 'Autor', + 'community' => 'Rozšírenia od komunity', + 'description' => 'Popis', + 'disabled' => 'Zakázané', + 'empty_list' => 'Žiadne nainštalované rozšírenia', + 'enabled' => 'Povolené', + 'latest' => 'Nainštalované', + 'name' => 'Názov', + 'no_configure_view' => 'Toto rozšírenie nemá nastavenia.', + 'system' => array( + '_' => 'Systémové rozšírenia', + 'no_rights' => 'Systémové rozšírenie (nemáte oprávnenia)', + ), + 'title' => 'Rozšírenia', + 'update' => 'Sú dostupné aktualizácie', + 'user' => 'Používateľské rozšírenia', + 'version' => 'Verzia', + ), + 'stats' => array( + '_' => 'Štatistiky', + 'all_feeds' => 'Všetky kanály', + 'category' => 'Kategória', + 'entry_count' => 'Počet položiek', + 'entry_per_category' => 'Položiek v kategórii', + 'entry_per_day' => 'Položiek za deň (posledných 30 dní)', + 'entry_per_day_of_week' => 'Za deň v týždni (priemer: %.2f správy)', + 'entry_per_hour' => 'Za hodinu (priemer: %.2f správy)', + 'entry_per_month' => 'Za mesiac (priemer: %.2f správy)', + 'entry_repartition' => 'Rozdelenie článkov', + 'feed' => 'Kanál', + 'feed_per_category' => 'Kanálov v kategórii', + 'idle' => 'Neaktívne kanály', + 'main' => 'Hlavné štatistiky', + 'main_stream' => 'Všetky kanály', + 'menu' => array( + 'idle' => 'Neaktívne kanály', + 'main' => 'Hlavné štatistiky', + 'repartition' => 'Rozdelenie článkov', + ), + 'no_idle' => 'Žiadne neaktívne kanály!', + 'number_entries' => 'Počet článkov: %d', + 'percent_of_total' => 'Z celkového počtu: %%', + 'repartition' => 'Rozdelenie článkov', + 'status_favorites' => 'Obľúbené', + 'status_read' => 'Prečítané', + 'status_total' => 'Spolu', + 'status_unread' => 'Neprečítané', + 'title' => 'Štatistiky', + 'top_feed' => 'Top 10 kanálov', + ), + 'system' => array( + '_' => 'Nastavenia systému', + 'auto-update-url' => 'Odkaz na aktualizačný server', + 'instance-name' => 'Názov inštancie', + 'max-categories' => 'Limit počtu kategórií pre používateľa', + 'max-feeds' => 'Limit počtu kanálov pre používateľov', + 'cookie-duration' => array( + 'help' => 'v sekundách', + 'number' => 'Dobra, počas ktorej ste prihlásený', + ), + 'registration' => array( + 'help' => '0 znamená žiadny limit počtu účtov', + 'number' => 'Maximálny počt účtov', + ), + ), + 'update' => array( + '_' => 'Aktualizácia systému', + 'apply' => 'Použiť', + 'check' => 'Skontrolovať aktualizácie', + 'current_version' => 'Vaša aktuálna verzia FreshRSS: %s', + 'last' => 'Posledná kontrola: %s', + 'none' => 'Žiadna nová aktualizácia', + 'title' => 'Aktualizácia systému', + ), + 'user' => array( + 'articles_and_size' => '%s článkov (%s)', + 'create' => 'Vytvoriť nového používateľa', + 'delete_users' => 'Zmazať používateľa', + 'language' => 'Jazyk', + 'number' => 'Je vytvorený používateľ: %d', + 'numbers' => 'Je vytvorených používateľov: %d', + 'password_form' => 'Heslo
(pre spôsob prihlásenia cez webový formulár)', + 'password_format' => 'Minimálne 7 znakov', + 'selected' => 'Označený používateľ', + 'title' => 'Správa používateľov', + 'update_users' => 'Sktualizovať používateľov', + 'user_list' => 'Zoznam používateľov', + 'username' => 'Používateľské meno', + 'users' => 'Používatelia', + ), +); diff --git a/app/i18n/sk/conf.php b/app/i18n/sk/conf.php new file mode 100644 index 000000000..f704fd4be --- /dev/null +++ b/app/i18n/sk/conf.php @@ -0,0 +1,188 @@ + array( + '_' => 'Archivovanie', + 'advanced' => 'Pokročilé', + 'delete_after' => 'Vymazať články po', + 'help' => 'Viac možností nájdete v nastaveniach kanála', + 'keep_history_by_feed' => 'Minimálny počet článkov kanála na zachovanie', + 'optimize' => 'Optimalizovať databázu', + 'optimize_help' => 'Občas vykonajte na zmenšenie veľkosti databázy', + 'purge_now' => 'Vyčistiť teraz', + 'title' => 'Archivovanie', + 'ttl' => 'Neaktualizovať častejšie ako', + ), + 'display' => array( + '_' => 'Zobrazenie', + 'icon' => array( + 'bottom_line' => 'Spodný riadok', + 'display_authors' => 'Autori', + 'entry' => 'Ikony článku', + 'publication_date' => 'Dátum zverejnenia', + 'related_tags' => 'Značky článku', + 'sharing' => 'Zdieľanie', + 'top_line' => 'Horný riadok', + ), + 'language' => 'Jazyk', + 'notif_html5' => array( + 'seconds' => 'sekundy (0 znamená bez limitu)', + 'timeout' => 'Limit HTML5 oznámenia', + ), + 'show_nav_buttons' => 'Zobraziť tlačidlá oznámenia', + 'theme' => 'Vzhľad', + 'title' => 'Zobraziť', + 'width' => array( + 'content' => 'Šírka obsahu', + 'large' => 'Veľká', + 'medium' => 'Stredná', + 'no_limit' => 'Bez obmedzenia', + 'thin' => 'Úzka', + ), + ), + 'profile' => array( + '_' => 'Správca profilu', + 'delete' => array( + '_' => 'Vymazanie účtu', + 'warn' => 'Váš účet a všetky údaje v ňom budú vymazané.', + ), + 'password_api' => 'Heslo API
(pre mobilné aplikácie)', + 'password_form' => 'Heslo
(pre spôsob prihlásenia cez webový formulár)', + 'password_format' => 'Najmenej 7 znakov', + 'title' => 'Profil', + ), + 'query' => array( + '_' => 'Dopyty používateľa', + 'deprecated' => 'Tento dopyt už nie je platný. Kategória alebo kanál boli vymazané.', + 'display' => 'Zobraziť výsledky dopytu používateľa', + 'filter' => 'Použitý filter:', + 'get_all' => 'Zobraziť všetky články', + 'get_category' => 'Zobraziť kategóriu "%s"', + 'get_favorite' => 'Zobraziť obľúbené články', + 'get_feed' => 'Zobraziť kanál "%s"', + 'no_filter' => 'Žiadny filter', + 'none' => 'Zatiaľ ste nevytvorili používateľský dopyt.', + 'number' => 'Dopyt číslo %d', + 'order_asc' => 'Zobraziť staršie články hore', + 'order_desc' => 'Zobraziť novšie články hore', + 'remove' => 'Vymazať dopyt používateľa', + 'search' => 'Vyhľadáva sa: "%s"', + 'state_0' => 'Zobraziť všetky články', + 'state_1' => 'Zobraziť prečítané články', + 'state_2' => 'Zobraziť neprečítané články', + 'state_3' => 'Zobraziť všetky články', + 'state_4' => 'Zobraziť obľúbené články', + 'state_5' => 'Zobraziť prečítané obľúbené články', + 'state_6' => 'Zobraziť neprečítané obľúbené články', + 'state_7' => 'Zobraziť obľúbené články', + 'state_8' => 'Zobraziť neobľúbené články', + 'state_9' => 'Zobraziť prečítané neobľúbené články', + 'state_10' => 'Zobraziť neprečítané neobľúbené články', + 'state_11' => 'Zobraziť neobľúbené články', + 'state_12' => 'Zobraziť všetky články', + 'state_13' => 'Zobraziť prečítané články', + 'state_14' => 'Zobraziť neprečítané články', + 'state_15' => 'Zobraziť všetky články', + 'title' => 'Používateľské dopyty', + ), + 'reading' => array( + '_' => 'Čítanie', + 'after_onread' => 'Po “Označiť všetko ako prečítané”,', + 'articles_per_page' => 'Počet článkov na jednu stranu', + 'auto_load_more' => 'Načítať ďalšie články dolu na stránke', + 'auto_remove_article' => 'Skryť články po prečítaní', + 'confirm_enabled' => 'Zobraziť potvrdzovací dialóg po kliknutí na “Označiť všetko ako prečítané”', + 'display_articles_unfolded' => 'Zobraziť články otvorené', + 'display_categories_unfolded' => 'Zobraziť kategórie otvorené', + 'hide_read_feeds' => 'Skryť kategórie a kanály s nulovým počtom neprečítaných článkov (nefunguje s nastaveným “Zobraziť všetky články”)', + 'img_with_lazyload' => 'Pre načítanie obrázkov použiť "lazy load"', + 'jump_next' => 'skočiť na ďalší neprečítaný (kanál ale kategóriu)', + 'mark_updated_article_unread' => 'Označiť aktualizované články ako neprečítané', + 'number_divided_when_reader' => 'V režime čítania predeliť na dve časti.', + 'read' => array( + 'article_open_on_website' => 'keď je článok otvorený na svojej webovej stránke', + 'article_viewed' => 'keď je článok zobrazený', + 'scroll' => 'počas skrolovania', + 'upon_reception' => 'po načítaní článku', + 'when' => 'Označiť článok ako prečítaný…', + ), + 'show' => array( + '_' => 'Článkov na zobrazenie', + 'adaptive' => 'Vyberte zobrazenie', + 'all_articles' => 'Zobraziť všetky články', + 'unread' => 'Zobraziť iba neprečítané', + ), + 'sides_close_article' => 'Po kliknutí mimo textu článku sa článok zatvorí', + 'sort' => array( + '_' => 'Poradie', + 'newer_first' => 'Novšie hore', + 'older_first' => 'Staršie hore', + ), + 'sticky_post' => 'Po otvorení posunúť článok hore', + 'title' => 'Čítanie', + 'view' => array( + 'default' => 'Prednastavené zobrazenie', + 'global' => 'Prehľadné zobrazenie', + 'normal' => 'Základné zobrazenie', + 'reader' => 'Zobrazenie na čítanie', + ), + ), + 'sharing' => array( + '_' => 'Zdieľanie', + 'add' => 'Pridať spôsob zdieľania', + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'E-mail', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'more_information' => 'Viac informácií', + 'print' => 'Tlač', + 'remove' => 'Odstrániť spôsob zdieľania', + 'shaarli' => 'Shaarli', + 'share_name' => 'Meno pre zobrazenie', + 'share_url' => 'Zdieľaný odkaz', + 'title' => 'Zdieľanie', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'shortcut' => array( + '_' => 'Skratky', + 'article_action' => 'Akcie článku', + 'auto_share' => 'Zdieľať', + 'auto_share_help' => 'Ak je nastavený iba jeden spôsob zdieľania, použije sa. Inak si spôsoby zdieľania vyberá používateľ podľa čísla.', + 'close_dropdown' => 'Zavrie menu', + 'collapse_article' => 'Zroluje článok', + 'first_article' => 'Otvorí prvý článok', + 'focus_search' => 'Vyhľadávanie', + 'global_view' => 'Prepne do prehľadného zobrazenia', + 'help' => 'Zobrazí dokumentáciu', + 'javascript' => 'JavaScript musí byť povolený, ak chcete používať skratky', + 'last_article' => 'Otvorí posledný článok', + 'load_more' => 'Načíta viac článkov', + 'mark_favorite' => 'O(d)značí ako obľúbené', + 'mark_read' => 'O(d)značí ako prečítané', + 'navigation' => 'Navigácia', + 'navigation_help' => 'Po stlačení skratky s klávesou "Shift", sa skratky navigácie vzťahujú na kanály.
Po stlačení skratky s klávesou "Alt", sa skratky navigácie vzťahujú na kategórie.', + 'navigation_no_mod_help' => 'Tieto skratky navigácie nepodporujú klávesy "Shift" a "Alt".', + 'next_article' => 'Otvorí ďalší článok', + 'normal_view' => 'Prepne do základného zobrazenia', + 'other_action' => 'Ostatné akcie', + 'previous_article' => 'Otvorí predošlý článok', + 'reading_view' => 'Prepne do zobrazenia na čítanie', + 'rss_view' => 'Otvorí zobrazenie RSS v novej záložke', + 'see_on_website' => 'Zobrazí na webovej stránke', + 'shift_for_all_read' => '+ shift na označenie všetkých článkov ako prečítaných', + 'skip_next_article' => 'Prejde na ďalší bez otvorenia', + 'skip_previous_article' => 'Prejde na predošlý bez otvorenia', + 'title' => 'Skratky', + 'user_filter' => 'Použiť používateľské filtre', + 'user_filter_help' => 'Ak je nastavený iba jeden spôsob zdieľania, použije sa. Inak si spôsoby zdieľania vyberá používateľ podľa čísla.', + 'views' => 'Zobrazenia', + ), + 'user' => array( + 'articles_and_size' => '%s článkov (%s)', + 'current' => 'Aktuálny používateľ', + 'is_admin' => 'je administrátor', + 'users' => 'Používatelia', + ), +); diff --git a/app/i18n/sk/feedback.php b/app/i18n/sk/feedback.php new file mode 100644 index 000000000..9aee79068 --- /dev/null +++ b/app/i18n/sk/feedback.php @@ -0,0 +1,116 @@ + array( + 'optimization_complete' => 'Optimalizácia dokončená', + ), + 'access' => array( + 'denied' => 'Na prístup k tejto stránke nemáte oprávnenie', + 'not_found' => 'Hľadáte stránku, ktorá neexistuje', + ), + 'auth' => array( + 'form' => array( + 'not_set' => 'Nastavl problém pri nastavovaní prihlasovacieho systému. Prosím, skúste to znova neskôr.', + 'set' => 'Webový formulár je teraz váš prednastavený prihlasovací spôsob.', + ), + 'login' => array( + 'invalid' => 'Nesprávne prihlasovacie údaje', + 'success' => 'Úspešne ste sa prihlásili', + ), + 'logout' => array( + 'success' => 'Boli ste odhlásený', + ), + 'no_password_set' => 'Heslo administrátora nebolo nastavené. Táto funkcia nie je dostupná.', + ), + 'conf' => array( + 'error' => 'Vyskytla sa chyba počas ukladania nastavaní', + 'query_created' => 'Dopyt "%s" bol vytvorený.', + 'shortcuts_updated' => 'Skratky boli aktualizované', + 'updated' => 'Nastavenia boli aktualizované', + ), + 'extensions' => array( + 'already_enabled' => '%s už je povolené', + 'disable' => array( + 'ko' => '%s sa nepodarilo nainštalovať. Prečítajte si záznamy FreshRSS, ak chcete poznať podrobnosti.', + 'ok' => '%s je teraz zakázaný', + ), + 'enable' => array( + 'ko' => '%s sa nepodarilo povoliť. Prečítajte si záznamy FreshRSS, ak chcete poznať podrobnosti.', + 'ok' => '%s je teraz povolený', + ), + 'no_access' => 'Nemáte prístup k %s', + 'not_enabled' => '%s nie je povolený', + 'not_found' => '%s neexistuje', + ), + 'import_export' => array( + 'export_no_zip_extension' => 'ZIP rozšírenie sa na vašom serveri nenachádza. Prosím, skúste exportovať súbory pojednom.', + 'feeds_imported' => 'Váš kanál bol importovaný a bude aktualizovaný', + 'feeds_imported_with_errors' => 'Vaše kanály boli importované, ale vyskytli sa chyby', + 'file_cannot_be_uploaded' => 'Súbor sa nepodarilo nahrať!', + 'no_zip_extension' => 'ZIP rozšírenie sa na vašom serveri nenachádza.', + 'zip_error' => 'Počas importovania ZIP sa vyskytla chyba.', + ), + 'profile' => array( + 'error' => 'Váš profil nie je možné upraviť', + 'updated' => 'Váš profil bol upravený', + ), + 'sub' => array( + 'actualize' => 'Aktualizácia', + 'articles' => array( + 'marked_read' => 'Vybraté články boli označené ako prečítané.', + 'marked_unread' => 'Články boli označené ako neprečítané.', + ), + 'category' => array( + 'created' => 'Kategória %s bola vytvorená.', + 'deleted' => 'Kategória bola odstránená.', + 'emptied' => 'Kategória bola vyprázdnená', + 'error' => 'Nepodarilo sa aktualizovať kategóriu', + 'name_exists' => 'Názov kategórie už existuje.', + 'no_id' => 'Musíte zadať ID kategórie.', + 'no_name' => 'Názov kategórie nemôže byť prázdny.', + 'not_delete_default' => 'Nemôžete odstrániť prednastavenú kategóriu!', + 'not_exist' => 'Kategória neexistuje!', + 'over_max' => 'Dosiahli ste limit počtu kategórií (%d)', + 'updated' => 'Kategória bola aktualizovaná.', + ), + 'feed' => array( + 'actualized' => '%s bol aktualizovaný', + 'actualizeds' => 'RSS kanál bol aktualizovaný', + 'added' => 'RSS kanál %s bol pridaný', + 'already_subscribed' => 'Tento RSS kanál už odoberáte: %s', + 'deleted' => 'Kanál bol vymazaný', + 'error' => 'Kanál sa nepodarilo aktualizovať', + 'internal_problem' => 'Kanál sa nepodarilo pridať. Prečítajte si záznamy FreshRSS, ak chcete poznať podrobnosti. Skúste pridať kanál pomocou #force_feed v odkaze (URL).', + 'invalid_url' => 'Odkaz %s je neplatný', + 'n_actualized' => 'Počet aktualizovaných kanálov: %d', + 'n_entries_deleted' => 'Počet vymazaných článkov: %d', + 'no_refresh' => 'Žiadny kanál sa neaktualizoval…', + 'not_added' => 'Kanál %s sa nepodarilo pridať', + 'over_max' => 'Dosiahli ste limit počtu kanálov (%d)', + 'updated' => 'Kanál bol aktualizovaný', + ), + 'purge_completed' => 'Čistenie ukončené. Počet vymazaných článkov: %d', + ), + 'update' => array( + 'can_apply' => 'FreshRSS sa teraz aktualizuje na verziu %s.', + 'error' => 'Počas aktualizácie sa vyskytla chyba: %s', + 'file_is_nok' => 'Je dostupná nová verzia %s, ale skontrolujte prístupové práva priečinka %s. HTTP server musí mať právo doň zapisovať.', + 'finished' => 'Aktualizácia prebehla úspešne!', + 'none' => 'Žiadne aktualizácie', + 'server_not_found' => 'Nepodarilo sa nájsť server s aktualizáciami. [%s]', + ), + 'user' => array( + 'created' => array( + '_' => 'Používateľ %s bol vytvorený', + 'error' => 'Používateľ %s nebol vytvorený', + ), + 'deleted' => array( + '_' => 'Používateľ %s bol vymazaný', + 'error' => 'Používateľ %s nebol vymazaný', + ), + 'updated' => array( + '_' => 'Používateľ %s bol aktualizovaný', + 'error' => 'Používateľ %s nebol aktualizovaný', + ), + ), +); diff --git a/app/i18n/sk/gen.php b/app/i18n/sk/gen.php new file mode 100644 index 000000000..7f09d887d --- /dev/null +++ b/app/i18n/sk/gen.php @@ -0,0 +1,197 @@ + array( + 'actualize' => 'Aktualizovať', + 'back_to_rss_feeds' => '← Späť na vaše RSS kanály', + 'cancel' => 'Zrušiť', + 'create' => 'Vytvoriť', + 'disable' => 'Zakázať', + 'empty' => 'Vyprázdniť', + 'enable' => 'Povoliť', + 'export' => 'Exportovať', + 'filter' => 'Filtrovať', + 'import' => 'Importovať', + 'manage' => 'Spravovať', + 'mark_favorite' => 'Označiť ako obľúbené', + 'mark_read' => 'Označiť ako prečítané', + 'remove' => 'Odstrániť', + 'see_website' => 'Zobraziť webovú stránku', + 'submit' => 'Poslať', + 'truncate' => 'Vymazať všetky články', + 'update' => 'Aktualizovať', + ), + 'auth' => array( + 'email' => 'E-mailová adresa', + 'keep_logged_in' => 'Zostať prihlásený (počet dní: %s)', + 'login' => 'Prihlásiť', + 'logout' => 'Odhlásiť', + 'password' => array( + '_' => 'Heslo', + 'format' => 'Najmenej 7 znakov', + ), + 'registration' => array( + '_' => 'Nový účet', + 'ask' => 'Vytvoriť účet?', + 'title' => 'Vytvorenie účtu', + ), + 'reset' => 'Reset prihlásenia', + 'username' => array( + '_' => 'Používateľské meno', + 'admin' => 'Administrátorské používateľské meno', + 'format' => 'maximálne 16 alfanumerických znakov', + ), + ), + 'date' => array( + 'Apr' => '\\A\\p\\r\\í\\l', + 'apr' => 'Apr.', + 'april' => 'Apríl', + 'Aug' => '\\A\\u\\g\\u\\s\\t', + 'aug' => 'Aug.', + 'august' => 'August', + 'before_yesterday' => 'Predvčerom', + 'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r', + 'dec' => 'Dec.', + 'december' => 'December', + 'Feb' => '\\F\\e\\b\\r\\u\\á\\r', + 'feb' => 'Feb.', + 'february' => 'Február', + 'format_date' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y', + 'format_date_hour' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y \\a\\t H\\:i', + 'fri' => 'Pi', + 'Jan' => '\\J\\a\\n\\u\\á\\r', + 'jan' => 'Jan.', + 'january' => 'Január', + 'Jul' => '\\J\\ú\\l', + 'jul' => 'Júl', + 'july' => 'Júl', + 'Jun' => '\\J\\ú\\n', + 'jun' => 'Jún', + 'june' => 'Jún', + 'last_3_month' => 'Posledné 3 mesiace', + 'last_6_month' => 'Posledných 6 mesiacov', + 'last_month' => 'Posledný mesiac', + 'last_week' => 'Posledný týždeň', + 'last_year' => 'Posledný rok', + 'Mar' => '\\M\\a\\r\\e\\c', + 'mar' => 'Mar.', + 'march' => 'Marec', + 'May' => '\\M\\á\\j', + 'may' => 'Máj', + 'may_' => 'Máj', + 'mon' => 'Po', + 'month' => 'mesiace', + 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', + 'nov' => 'Nov.', + 'november' => 'November', + 'Oct' => '\\O\\k\\t\\ó\\b\\e\\r', + 'oct' => 'Okt.', + 'october' => 'Október', + 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', + 'sat' => 'So', + 'sep' => 'Sept.', + 'september' => 'September', + 'sun' => 'Ne', + 'thu' => 'Št', + 'today' => 'Dnes', + 'tue' => 'Ut', + 'wed' => 'St', + 'yesterday' => 'Včera', + ), + 'freshrss' => array( + '_' => 'FreshRSS', + 'about' => 'O FreshRSS', + ), + 'js' => array( + 'category_empty' => 'Prázdna kategória', + 'confirm_action' => 'Určite chcete vykonať túto akciu? Zmeny budú nezvratné!', + 'confirm_action_feed_cat' => 'Určite chcete vykonať túto akciu? Prídete o súvisiace obľúbené a používateľské dopyty. Zmeny budú nezvratné!', + 'feedback' => array( + 'body_new_articles' => 'Počet nových článkov v čítačke FreshRSS: %%d', + 'request_failed' => 'Nepodarilo sa spracovať váš dopyt, pravdepodobne kvôli problému s pripojením do internetu.', + 'title_new_articles' => 'FreshRSS: nové články!', + ), + 'new_article' => 'Našli sa nové články. Kliknite na obnovenie stránky.', + 'should_be_activated' => 'Musíte povoliť JavaScript', + ), + 'lang' => array( + 'cz' => 'Čeština', + 'sk' => 'Slovenčina', + 'de' => 'Deutsch', + 'en' => 'English', + 'es' => 'Español', + 'fr' => 'Français', + 'he' => 'עברית', + 'it' => 'Italiano', + 'kr' => '한국어', + 'nl' => 'Nederlands', + 'oc' => 'Occitan', + 'pt-br' => 'Português (Brasil)', + 'ru' => 'Русский', + 'tr' => 'Türkçe', + 'zh-cn' => '简体中文', + ), + 'menu' => array( + 'about' => 'O FreshRSS', + 'admin' => 'Administrácia', + 'archiving' => 'Archivácia', + 'authentication' => 'Prihlásenie', + 'check_install' => 'Kontroloa inštalácie', + 'configuration' => 'Nastavenia', + 'display' => 'Zobrazenie', + 'extensions' => 'Rozšírenia', + 'logs' => 'Záznamy', + 'queries' => 'Používateľské dopyty', + 'reading' => 'Čítanie', + 'search' => 'Hľadajte slová alebo #značky', + 'sharing' => 'Zdieľanie', + 'shortcuts' => 'Skratky', + 'stats' => 'Štatistiky', + 'system' => 'Nastavenie systému', + 'update' => 'Aktualizácia', + 'user_management' => 'Spravovať používateľov', + 'user_profile' => 'Profil', + ), + 'pagination' => array( + 'first' => 'Prvý', + 'last' => 'Posledný', + 'load_more' => 'Načítať viac článkov', + 'mark_all_read' => 'Označiť všetko prečítané', + 'next' => 'Ďalší', + 'nothing_to_load' => 'Žiadne nové články', + 'previous' => 'Predošlý', + ), + 'share' => array( + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'E-mail', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'gnusocial' => 'GNU social', + 'jdh' => 'Journal du hacker', + 'Known' => 'Stránky založené na Known', + 'linkedin' => 'LinkedIn', + 'mastodon' => 'Mastodon', + 'movim' => 'Movim', + 'pinboard' => 'Pinboard', + 'pocket' => 'Pocket', + 'print' => 'Print', + 'shaarli' => 'Shaarli', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag v1', + 'wallabagv2' => 'wallabag v2', + ), + 'short' => array( + 'attention' => 'Upozornenie!', + 'blank_to_disable' => 'Ak chcete zakázať, ponechajte prázdne', + 'by_author' => 'Od:', + 'by_default' => 'Prednastavené', + 'damn' => 'Sakra!', + 'default_category' => 'Bez kategórie', + 'no' => 'Nie', + 'not_applicable' => 'Nie je k dispozícii', + 'ok' => 'OK', + 'or' => 'alebo', + 'yes' => 'Áno', + ), +); diff --git a/app/i18n/sk/index.php b/app/i18n/sk/index.php new file mode 100644 index 000000000..19e63e720 --- /dev/null +++ b/app/i18n/sk/index.php @@ -0,0 +1,63 @@ + array( + '_' => 'O FreshRSS', + 'agpl3' => 'AGPL 3', + 'bugs_reports' => 'Nahlásiť chybu', + 'credits' => 'Poďakovanie', + 'credits_content' => 'Niektoré časti vzhľadu pochádzajú z Bootstrapu, aj keď FreshRSS tento framework nepoužíva. Ikony sú z GNOME project. Font Open Sans zabezpečil Steve Matteson. FreshRSS je založený na PHP frameworku Minz.', + 'freshrss_description' => 'FreshRSS je čítačka RSS kanálov, ktorú môžete nasadiť na vlastný server podobne ako Kriss Feed alebo Leed. Ide o jednoduchý a zároveň dobre nastaviteľný nástroj.', + 'github' => 'na Githube', + 'license' => 'Licencia', + 'project_website' => 'Webová stránka projektu', + 'title' => 'O FreshRSS', + 'version' => 'Verzia', + 'website' => 'Webová stránka', + ), + 'feed' => array( + 'add' => 'Môžete pridať kanály.', + 'empty' => 'Žiadne články.', + 'rss_of' => 'RSS kanál pre %s', + 'title' => 'Vaše RSS kanály', + 'title_global' => 'Prehľad', + 'title_fav' => 'Vaše obľúbené', + ), + 'log' => array( + '_' => 'Záznamy', + 'clear' => 'Vymazať záznamy', + 'empty' => 'Súbor záznamu je prázdny', + 'title' => 'Záznamy', + ), + 'menu' => array( + 'about' => 'O FreshRSS', + 'add_query' => 'Vytvoriť dopyt', + 'before_one_day' => 'Pred 1 dňom', + 'before_one_week' => 'Pred 1 týždňom', + 'favorites' => 'Obľúbené (%s)', + 'global_view' => 'Prehľad', + 'main_stream' => 'Všetky kanály', + 'mark_all_read' => 'Označiť všetko ako prečítané', + 'mark_cat_read' => 'Označiť kategóriu ako prečítanú', + 'mark_feed_read' => 'Označiť kanál ako prečítaný', + 'mark_selection_unread' => 'Označiť označené ako prečítané', + 'newer_first' => 'Novšie hore', + 'non-starred' => 'Zobraziť všetko okrem obľúbených', + 'normal_view' => 'Základné zobrazenie', + 'older_first' => 'Staršie hore', + 'queries' => 'Používateľské dopyty', + 'read' => 'Zobraziť prečítané', + 'reader_view' => 'Zobrazenie na čítanie', + 'rss_view' => 'RSS kanál', + 'search_short' => 'Hľadať', + 'starred' => 'Zobraziť obľúbené', + 'stats' => 'Štatistiky', + 'subscription' => 'Správca odberov', + 'tags' => 'Moje nálepky', + 'unread' => 'Zobraziť neprečítané', + ), + 'share' => 'Zdieľať', + 'tag' => array( + 'related' => 'Značky článku', + ), +); diff --git a/app/i18n/sk/install.php b/app/i18n/sk/install.php new file mode 100644 index 000000000..08fbfeef9 --- /dev/null +++ b/app/i18n/sk/install.php @@ -0,0 +1,123 @@ + array( + 'finish' => 'Dokončiť inštaláciu', + 'fix_errors_before' => 'Prosím, pred pokračovaním opravte chyby.', + 'keep_install' => 'Použiť predošlé nastavenia', + 'next_step' => 'Ďalší krok', + 'reinstall' => 'Preinštalovať FreshRSS', + ), + 'auth' => array( + 'form' => 'Webový formulár (tradičný, vyžaduje JavaScript)', + 'http' => 'HTTP (pre pokročilých používateľov s HTTPS)', + 'none' => 'Žiadny (nebezpečné)', + 'password_form' => 'Heslo
(pre prihlásenie cez webový formulár)', + 'password_format' => 'Najmenej 7 znakov', + 'type' => 'Spôsob prihlásenia', + ), + 'bdd' => array( + '_' => 'Databáza', + 'conf' => array( + '_' => 'Nastavenia databázy', + 'ko' => 'Skontrolovať vaše informácie o databáze.', + 'ok' => 'Nastavenia databázy boli uložené.', + ), + 'host' => 'Server', + 'password' => 'Heslo databázy', + 'prefix' => 'Predpona názvu tabuľky', + 'type' => 'Druh databázy', + 'username' => 'Používateľské meno databázy', + ), + 'check' => array( + '_' => 'Kontrola', + 'already_installed' => 'Zistilo sa, že FreshRSS je už nainštalovaný!', + 'cache' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data/cache. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku vyrovnávacej pamäte sú OK.', + ), + 'ctype' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na kontrolu typu znakov (php-ctype).', + 'ok' => 'Našla sa požadovaná knižnica na kontrolu typu znakov (ctype).', + ), + 'curl' => array( + 'nok' => 'Nepodarilo sa nájsť knižnicu cURL (balík php-curl).', + 'ok' => 'Našla sa knižnica cURL.', + ), + 'data' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku údajov sú OK.', + ), + 'dom' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na prehliadanie DOM.', + 'ok' => 'Našla sa požadovaná knižnica na prehliadanie DOM.', + ), + 'favicons' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data/favicons. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku ikôn obľúbených sú OK.', + ), + 'fileinfo' => array( + 'nok' => 'Nepodarilo sa nájsť knižniuc PHP fileinfo (balík fileinfo).', + 'ok' => 'Našla sa knižnica fileinfo.', + ), + 'http_referer' => array( + 'nok' => 'Prosím, skontrolujte, či ste nezmenili váš HTTP REFERER.', + 'ok' => 'Váš HTTP REFERER je OK.', + ), + 'json' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na spracovanie formátu JSON.', + 'ok' => 'Našla sa požadovaná knižnica na spracovanie formátu JSON.', + ), + 'mbstring' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu mbstring pre Unicode.', + 'ok' => 'Našla sa požadovaná knižnica mbstring pre Unicode.', + ), + 'minz' => array( + 'nok' => 'Nepodarilo sa nájsť framework Minz.', + 'ok' => 'Našiel sa framework Minz.', + ), + 'pcre' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu pre regulárne výrazy (php-pcre).', + 'ok' => 'Našla sa požadovaná knižnica pre regulárne výrazy (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Nepodarilo sa nájsť PDO alebo niektorý z podporovaných ovládačov (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Našiel sa PDO a aspoň jeden z podporovaných ovládačov (pdo_mysql, pdo_sqlite, pdo_pgsql).', + ), + 'php' => array( + 'nok' => 'Vaša verzia PHP je %s, ale FreshRSS vyžaduje minimálne verziu %s.', + 'ok' => 'Vaša verzia PHP %s je kompatibilná s FreshRSS.', + ), + 'users' => array( + 'nok' => 'Skontrolujte oprávnenia prístupu do priečinku ./data/users. HTTP server musí mať právo doň zapisovať.', + 'ok' => 'Oprávnenia prístupu do priečinku používateľov sú OK.', + ), + 'xml' => array( + 'nok' => 'Nepodarilo sa nájsť požadovanú knižnicu na spracovanie formátu XML.', + 'ok' => 'Našla sa požadovaná knižnica na spracovanie formátu XML.', + ), + ), + 'conf' => array( + '_' => 'Hlavné nastavenia', + 'ok' => 'Hlavné nastavenia boli uložené.', + ), + 'congratulations' => 'Nastavenia!', + 'default_user' => 'Hlavné používateľské meno (najviac 16 alfanumerických znakov)', + 'delete_articles_after' => 'Vymazať články po', + 'fix_errors_before' => 'Prosím, pred pokračovaním opravte chyby.', + 'javascript_is_better' => 'FreshRSS si užijete viac, keď povolíte JavaScript', + 'js' => array( + 'confirm_reinstall' => 'Ak budete pokračovať v preinštalovaní FreshRSS, stratíte vaše predošlé nastavenia. Naozaj chcete pokračovať?', + ), + 'language' => array( + '_' => 'Jazyk', + 'choose' => 'Vyberte jazyk pre FreshRSS', + 'defined' => 'Jazyk bol nastavený.', + ), + 'not_deleted' => 'Niečo sa nepodarilo. Musíte ručne zmazať súbor %s.', + 'ok' => 'Inštalácia bola úspešná.', + 'step' => 'krok %d', + 'steps' => 'Kroky', + 'title' => 'Inštalácia · FreshRSS', + 'this_is_the_end' => 'Toto je koniec', +); diff --git a/app/i18n/sk/sub.php b/app/i18n/sk/sub.php new file mode 100644 index 000000000..4dcd09f57 --- /dev/null +++ b/app/i18n/sk/sub.php @@ -0,0 +1,100 @@ + array( + 'documentation' => 'Skopírujte tento odkaz a použite ho v inom programe.', + 'title' => 'API', + ), + 'bookmarklet' => array( + 'documentation' => 'Presunte toto tlačidlo do vašich záložiek, alebo kliknite pravým a zvoľte "Uložiť odkaz do záložiek". Potom kliknite na tlačidlo "Odoberať" na ktorejkoľvek stránke, ktorú chcete odoberať.', + 'label' => 'Odoberať', + 'title' => 'Záložka', + ), + 'category' => array( + '_' => 'Kategória', + 'add' => 'Pridať kategóriu', + 'empty' => 'Prázdna kategória', + 'information' => 'Informácia', + 'new' => 'Nová kategória', + 'title' => 'Názov', + ), + 'feed' => array( + 'add' => 'Pridať RSS kanál', + 'advanced' => 'Pokročilé', + 'archiving' => 'Archivovanie', + 'auth' => array( + 'configuration' => 'Prihlásenie', + 'help' => 'Povoliť prístup do kanálov chránených cez HTTP.', + 'http' => 'Prihlásenie cez HTTP', + 'password' => 'Heslo pre HTTP', + 'username' => 'Používateľské meno pre HTTP', + ), + 'clear_cache' => 'Vždy vymazať vyrovnávaciu pamäť', + 'css_help' => 'Stiahnuť skrátenú verziu RSS kanála (pozor, vyžaduje viac času!)', + 'css_path' => 'Pôvodný CSS súbor článku z webovej stránky', + 'description' => 'Popis', + 'empty' => 'Tento kanál je prázdny. Overte, prosím, či je ešte spravovaný autorom.', + 'error' => 'Vyskytol sa problém s týmto kanálom. Overte, prosím, či kanál stále existuje, potom ho obnovte.', + 'filteractions' => array( + '_' => 'Filtrovať akcie', + 'help' => 'Napíšte jeden výraz hľadania na riadok.', + ), + 'information' => 'Informácia', + 'keep_history' => 'Minimálny počet článkov na uchovanie', + 'moved_category_deleted' => 'Keď vymažete kategóriu, jej kanály sa automaticky zaradia pod %s.', + 'mute' => 'stíšiť', + 'no_selected' => 'Nevybrali ste kanál.', + 'number_entries' => 'Počet článkov: %d', + 'priority' => array( + '_' => 'Viditeľnosť', + 'archived' => 'Nezobrazovať (archivované)', + 'main_stream' => 'Zobraziť v prehľade kanálov', + 'normal' => 'Zobraziť vo svojej kategórii', + ), + 'websub' => 'Okamžité oznámenia cez WebSub', + 'show' => array( + 'all' => 'Zobraziť všetky kanály', + 'error' => 'Zobraziť iba kanály s chybou', + ), + 'showing' => array( + 'error' => 'Zobraziť iba kanály s chybou', + ), + 'ssl_verify' => 'Overiť bezpečnosť SSL', + 'stats' => 'Štatistiky', + 'think_to_add' => 'Mali by ste pridať kanály.', + 'timeout' => 'Doba platnosti dá v sekundách', + 'title' => 'Nadpis', + 'title_add' => 'Pridať kanál RSS', + 'ttl' => 'Automaticky neaktualizovať častejšie ako', + 'url' => 'Odkaz kanála', + 'validator' => 'Skontrolovať platnosť kanála', + 'website' => 'Odkaz webovej stránky', + ), + 'firefox' => array( + 'documentation' => 'Pridajte RSS kanály do Firefoxu pomocou tohto návodu.', + 'title' => 'RSS čítačka vo Firefoxe', + ), + 'import_export' => array( + 'export' => 'Exportovať', + 'export_opml' => 'Exportovať zoznam kanálov (OPML)', + 'export_starred' => 'Exportovať vaše obľúbené', + 'export_labelled' => 'Exportovať vaše označené články', + 'feed_list' => 'Zoznam článkov %s', + 'file_to_import' => 'Súbor na import
(OPML, JSON alebo ZIP)', + 'file_to_import_no_zip' => 'Súbor na import
(OPML alebo JSON)', + 'import' => 'Importovať', + 'starred_list' => 'Zoznam obľúbených článkov', + 'title' => 'Import / export', + ), + 'menu' => array( + 'bookmark' => 'Odoberať (záložka FreshRSS)', + 'import_export' => 'Import / export', + 'subscription_management' => 'Správa odoberaných kanálov', + 'subscription_tools' => 'Nástroje na odoberanie kanálov', + ), + 'title' => array( + '_' => 'Správa odoberaných kanálov', + 'feed_management' => 'Správa RSS kanálov', + 'subscription_tools' => 'Nástroje na odoberanie kanálov', + ), +); diff --git a/cli/i18n/ignore/sk.php b/cli/i18n/ignore/sk.php new file mode 100644 index 000000000..47d20f81f --- /dev/null +++ b/cli/i18n/ignore/sk.php @@ -0,0 +1,66 @@ + Date: Wed, 21 Aug 2019 09:27:32 +0200 Subject: [i18n] Finish Dutch translation (#2503) Add a couple of new strings, minor grammar and style improvements, and ignore everything that should be ignored for 100 %. --- app/i18n/nl/feedback.php | 2 +- app/i18n/nl/gen.php | 17 +---------------- app/i18n/nl/index.php | 12 ++++++------ app/i18n/nl/sub.php | 18 +++++++++--------- cli/i18n/ignore/nl.php | 11 +++++++++++ 5 files changed, 28 insertions(+), 32 deletions(-) (limited to 'cli') diff --git a/app/i18n/nl/feedback.php b/app/i18n/nl/feedback.php index 25378360b..97e1a71b8 100644 --- a/app/i18n/nl/feedback.php +++ b/app/i18n/nl/feedback.php @@ -75,7 +75,7 @@ return array( ), 'feed' => array( 'actualized' => '%s vernieuwd', - 'actualizeds' => 'RSS feeds vernieuwd', + 'actualizeds' => 'RSS-feeds vernieuwd', 'added' => 'RSS feed %s toegevoegd', 'already_subscribed' => 'Al geabonneerd op %s', 'deleted' => 'Feed verwijderd', diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php index c7c97d1e0..57d68d673 100644 --- a/app/i18n/nl/gen.php +++ b/app/i18n/nl/gen.php @@ -161,23 +161,8 @@ return array( 'previous' => 'Vorige', ), 'share' => array( - 'blogotext' => 'Blogotext', - 'diaspora' => 'Diaspora*', 'email' => 'Email', - 'facebook' => 'Facebook', - 'gnusocial' => 'GNU social', - 'jdh' => 'Journal du hacker', - 'Known' => 'Known based sites', - 'linkedin' => 'LinkedIn', - 'mastodon' => 'Mastodon', - 'movim' => 'Movim', - 'pinboard' => 'Pinboard', - 'pocket' => 'Pocket', - 'print' => 'Print', - 'shaarli' => 'Shaarli', - 'twitter' => 'Twitter', - 'wallabag' => 'wallabag v1', - 'wallabagv2' => 'wallabag v2', + 'Known' => 'Known-gebaseerde sites', ), 'short' => array( 'attention' => 'Attentie!', diff --git a/app/i18n/nl/index.php b/app/i18n/nl/index.php index d202b812a..5f71a180f 100644 --- a/app/i18n/nl/index.php +++ b/app/i18n/nl/index.php @@ -7,10 +7,10 @@ return array( 'bugs_reports' => 'Rapporteer fouten', 'credits' => 'Waarderingen', 'credits_content' => 'Sommige ontwerp elementen komen van Bootstrap alhoewel FreshRSS dit raamwerk niet gebruikt. Pictogrammen komen van het GNOME project. De Open Sans font police is gemaakt door Steve Matteson. FreshRSS is gebaseerd op Minz, een PHP raamwerk. Nederlandse vertaling door Wanabo, NieuwsKop.be. Link naar de Nederlandse vertaling, FreshRSS-Dutch-translation.', - 'freshrss_description' => 'FreshRSS is een RSS feed aggregator om zelf te hosten zoals Kriss Feed of Leed. Het gebruikt weinig systeembronnen en is makkelijk te administreren terwijl het een krachtig en makkelijk te configureren programma is.', + 'freshrss_description' => 'FreshRSS is een RSS-feed aggregator om zelf te hosten, net als Kriss Feed of Leed. Het gebruikt weinig systeembronnen en is makkelijk te beheren terwijl het een krachtig en makkelijk te configureren programma is.', 'github' => 'op Github', - 'license' => 'License', - 'project_website' => 'Project website', + 'license' => 'Licentie', + 'project_website' => 'Projectwebsite', 'title' => 'Over', 'version' => 'Versie', 'website' => 'Website', @@ -18,8 +18,8 @@ return array( 'feed' => array( 'add' => 'U kunt wat feeds toevoegen.', 'empty' => 'Er is geen artikel om te laten zien.', - 'rss_of' => 'RSS feed van %s', - 'title' => 'Overzicht RSS feeds', + 'rss_of' => 'RSS-feed van %s', + 'title' => 'Overzicht RSS-feeds', 'title_global' => 'Globale weergave', 'title_fav' => 'Uw favorieten', ), @@ -48,7 +48,7 @@ return array( 'queries' => 'Gebruikers queries', 'read' => 'Laat alleen gelezen zien', 'reader_view' => 'Lees modus', - 'rss_view' => 'RSS feed', + 'rss_view' => 'RSS-feed', 'search_short' => 'Zoeken', 'starred' => 'Laat alleen favorieten zien', 'stats' => 'Statistieken', diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index b59515f42..8ceb5aa28 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -19,25 +19,25 @@ return array( 'title' => 'Titel', ), 'feed' => array( - 'add' => 'Voeg een RSS feed toe', + 'add' => 'Voeg een RSS-feed toe', 'advanced' => 'Geavanceerd', 'archiving' => 'Archiveren', 'auth' => array( 'configuration' => 'Log in', - 'help' => 'Verbinding toestaan toegang te krijgen tot HTTP beveiligde RSS feeds', + 'help' => 'Verbinding toestaan toegang te krijgen tot HTTP beveiligde RSS-feeds', 'http' => 'HTTP Authenticatie', 'password' => 'HTTP wachtwoord', 'username' => 'HTTP gebruikers naam', ), 'clear_cache' => 'Cache altijd leegmaken', - 'css_help' => 'Haalt verstoorde RSS feeds op (attentie, heeft meer tijd nodig!)', - 'css_path' => 'Artikelen CSS pad op originele website', + 'css_help' => 'Haalt onvolledige RSS-feeds op (attentie, heeft meer tijd nodig!)', + 'css_path' => 'CSS-pad van artikelen op originele website', 'description' => 'Omschrijving', 'empty' => 'Deze feed is leeg. Controleer of deze nog actueel is.', 'error' => 'Deze feed heeft problemen. Verifieer a.u.b het doeladres en actualiseer het.', 'filteractions' => array( - '_' => 'Filter actions', //TODO - Translation - 'help' => 'Write one search filter per line.', //TODO - Translation + '_' => 'Filteracties', + 'help' => 'Voer één zoekfilter per lijn in.', ), 'information' => 'Informatie', 'keep_history' => 'Minimum aantal artikelen om te houden', @@ -64,11 +64,11 @@ return array( 'think_to_add' => 'Voeg wat feeds toe.', 'timeout' => 'Time-out in seconden', 'title' => 'Titel', - 'title_add' => 'Voeg een RSS feed toe', + 'title_add' => 'Voeg een RSS-feed toe', 'ttl' => 'Vernieuw automatisch niet vaker dan', - 'url' => 'Feed URL', + 'url' => 'Feed-url', 'validator' => 'Controleer de geldigheid van de feed', - 'website' => 'Website URL', + 'website' => 'Website-url', ), 'firefox' => array( 'documentation' => 'Volg de stappen die hier beschreven worden om FreshRSS aan de Firefox-nieuwslezerlijst toe te voegen.', diff --git a/cli/i18n/ignore/nl.php b/cli/i18n/ignore/nl.php index 6d28d68ed..f5a7205e8 100644 --- a/cli/i18n/ignore/nl.php +++ b/cli/i18n/ignore/nl.php @@ -1,6 +1,8 @@ Date: Thu, 29 Aug 2019 12:02:05 +0200 Subject: Provide email address verification feature (#2481) * Add an email field to the profile page I reuse the `mail_login` from the configuration. I'm not sure if it's useful today (I would say it was used when Persona login was available). A good improvement would be to rename `mail_login` into `email` so it would be more intuitive to use. * Add boolean to the conf to force email validation This commit only adds a configuration item. * Add email during registration if email must be validated * Set email token to validate when email changes * Block access to FreshRSS if email is not validated * Send email when address is changed * Allow to resend the validation email * Allow the user to change its email while blocked * Document the email validation feature * fixup! Allow the user to change its email while blocked * tec: Autoload PHPMailer lib * Validate email address format * Add feedback on validation email resend action * Allow to logout when user is blocked * fix: Change default email "from" * Reorganize i18n keys * Complete all the locales with default english * Hide sidebar (profile page) if email is not validated * Check email requirements on registration * Allow admin to specify email when creating users * Don't check email format if value is empty * Remove trailing comma in userController Co-Authored-By: Alexandre Alapetite * Set PHPMailer validator to html5 before sending email * fixup! Remove trailing comma in userController --- app/Controllers/authController.php | 1 + app/Controllers/configureController.php | 12 + app/Controllers/userController.php | 195 +- app/FreshRSS.php | 18 + app/Mailers/UserMailer.php | 31 + app/Models/ConfigurationSetter.php | 4 + app/i18n/cz/admin.php | 1 + app/i18n/cz/conf.php | 1 + app/i18n/cz/gen.php | 1 + app/i18n/cz/user.php | 32 + app/i18n/de/admin.php | 1 + app/i18n/de/conf.php | 1 + app/i18n/de/gen.php | 1 + app/i18n/de/user.php | 32 + app/i18n/en/admin.php | 1 + app/i18n/en/conf.php | 1 + app/i18n/en/gen.php | 1 + app/i18n/en/user.php | 32 + app/i18n/es/admin.php | 1 + app/i18n/es/conf.php | 1 + app/i18n/es/gen.php | 1 + app/i18n/es/user.php | 32 + app/i18n/fr/admin.php | 1 + app/i18n/fr/conf.php | 1 + app/i18n/fr/gen.php | 1 + app/i18n/fr/user.php | 32 + app/i18n/he/admin.php | 1 + app/i18n/he/conf.php | 1 + app/i18n/he/gen.php | 1 + app/i18n/he/user.php | 32 + app/i18n/it/admin.php | 1 + app/i18n/it/conf.php | 1 + app/i18n/it/gen.php | 1 + app/i18n/it/user.php | 32 + app/i18n/kr/admin.php | 1 + app/i18n/kr/conf.php | 1 + app/i18n/kr/gen.php | 1 + app/i18n/kr/user.php | 32 + app/i18n/nl/admin.php | 1 + app/i18n/nl/conf.php | 1 + app/i18n/nl/gen.php | 1 + app/i18n/nl/user.php | 32 + app/i18n/oc/admin.php | 1 + app/i18n/oc/conf.php | 1 + app/i18n/oc/gen.php | 1 + app/i18n/oc/user.php | 32 + app/i18n/pt-br/admin.php | 1 + app/i18n/pt-br/conf.php | 1 + app/i18n/pt-br/gen.php | 1 + app/i18n/pt-br/user.php | 32 + app/i18n/ru/admin.php | 1 + app/i18n/ru/conf.php | 1 + app/i18n/ru/gen.php | 1 + app/i18n/ru/user.php | 32 + app/i18n/tr/admin.php | 1 + app/i18n/tr/conf.php | 1 + app/i18n/tr/gen.php | 1 + app/i18n/tr/user.php | 32 + app/i18n/zh-cn/admin.php | 1 + app/i18n/zh-cn/conf.php | 1 + app/i18n/zh-cn/gen.php | 1 + app/i18n/zh-cn/user.php | 32 + app/layout/simple.phtml | 66 + app/views/auth/register.phtml | 9 + app/views/configure/system.phtml | 20 +- app/views/user/manage.phtml | 11 + app/views/user/profile.phtml | 13 +- app/views/user/validateEmail.phtml | 22 + app/views/user_mailer/email_need_validation.txt | 5 + cli/create-user.php | 7 +- cli/update-user.php | 1 + config-user.default.php | 1 + config.default.php | 9 +- docs/en/admins/01_Index.md | 3 +- docs/en/admins/05_Configuring_email_validation.md | 73 + lib/Minz/Mailer.php | 6 +- lib/Minz/Request.php | 7 + lib/PHPMailer/Exception.php | 39 - lib/PHPMailer/PHPMailer.php | 4502 --------------------- lib/PHPMailer/PHPMailer/Exception.php | 39 + lib/PHPMailer/PHPMailer/PHPMailer.php | 4502 +++++++++++++++++++++ lib/PHPMailer/PHPMailer/SMTP.php | 1326 ++++++ lib/PHPMailer/SMTP.php | 1326 ------ lib/lib_rss.php | 16 + 84 files changed, 6869 insertions(+), 5885 deletions(-) create mode 100644 app/Mailers/UserMailer.php create mode 100644 app/i18n/cz/user.php create mode 100644 app/i18n/de/user.php create mode 100644 app/i18n/en/user.php create mode 100644 app/i18n/es/user.php create mode 100644 app/i18n/fr/user.php create mode 100644 app/i18n/he/user.php create mode 100644 app/i18n/it/user.php create mode 100644 app/i18n/kr/user.php create mode 100644 app/i18n/nl/user.php create mode 100644 app/i18n/oc/user.php create mode 100644 app/i18n/pt-br/user.php create mode 100644 app/i18n/ru/user.php create mode 100644 app/i18n/tr/user.php create mode 100644 app/i18n/zh-cn/user.php create mode 100644 app/layout/simple.phtml create mode 100644 app/views/user/validateEmail.phtml create mode 100644 app/views/user_mailer/email_need_validation.txt create mode 100644 docs/en/admins/05_Configuring_email_validation.md delete mode 100644 lib/PHPMailer/Exception.php delete mode 100644 lib/PHPMailer/PHPMailer.php create mode 100644 lib/PHPMailer/PHPMailer/Exception.php create mode 100644 lib/PHPMailer/PHPMailer/PHPMailer.php create mode 100644 lib/PHPMailer/PHPMailer/SMTP.php delete mode 100644 lib/PHPMailer/SMTP.php (limited to 'cli') diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index e06a26399..a8b21b886 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -205,6 +205,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { Minz_Error::error(403); } + $this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation; Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · '); } } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index a839f0005..b02ad02e4 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -293,15 +293,24 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * configuration values then sends a notification to the user. * * The options available on the page are: + * - instance name (default: FreshRSS) + * - auto update URL (default: false) + * - force emails validation (default: false) * - user limit (default: 1) * - user category limit (default: 16384) * - user feed limit (default: 16384) * - user login duration for form auth (default: 2592000) + * + * The `force-email-validation` is ignored with PHP < 5.5 */ public function systemAction() { if (!FreshRSS_Auth::hasAccess('admin')) { Minz_Error::error(403); } + + $can_enable_email_validation = version_compare(PHP_VERSION, '5.5') >= 0; + $this->view->can_enable_email_validation = $can_enable_email_validation; + if (Minz_Request::isPost()) { $limits = FreshRSS_Context::$system_conf->limits; $limits['max_registrations'] = Minz_Request::param('max-registrations', 1); @@ -311,6 +320,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { FreshRSS_Context::$system_conf->limits = $limits; FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS'); FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false); + if ($can_enable_email_validation) { + FreshRSS_Context::$system_conf->force_email_validation = Minz_Request::param('force-email-validation', false); + } FreshRSS_Context::$system_conf->save(); invalidateHttpCache(); diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index c1c27a4ab..9e909a3b5 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -33,12 +33,23 @@ class FreshRSS_user_Controller extends Minz_ActionController { return false; } - public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { + public static function updateUser($user, $email, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { $userConfig = get_user_configuration($user); if ($userConfig === null) { return false; } + if ($email !== null && $userConfig->mail_login !== $email) { + $userConfig->mail_login = $email; + + if (FreshRSS_Context::$system_conf->force_email_validation) { + $salt = FreshRSS_Context::$system_conf->salt; + $userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true)); + $mailer = new FreshRSS_User_Mailer(); + $mailer->send_email_need_validation($user, $userConfig); + } + } + if ($passwordPlain != '') { $passwordHash = self::hashPassword($passwordPlain); $userConfig->passwordHash = $passwordHash; @@ -84,7 +95,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true); $username = Minz_Request::param('username'); - $ok = self::updateUser($username, $passwordPlain, $apiPasswordPlain, array( + $ok = self::updateUser($username, null, $passwordPlain, $apiPasswordPlain, array( 'token' => Minz_Request::param('token', null), )); @@ -111,25 +122,58 @@ class FreshRSS_user_Controller extends Minz_ActionController { Minz_Error::error(403); } + $email_not_verified = FreshRSS_Context::$user_conf->email_validation_token !== ''; + if ($email_not_verified) { + $this->view->_layout('simple'); + $this->view->disable_aside = true; + } + Minz_View::prependTitle(_t('conf.profile.title') . ' · '); Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'))); if (Minz_Request::isPost()) { + $system_conf = FreshRSS_Context::$system_conf; + $user_config = FreshRSS_Context::$user_conf; + $old_email = $user_config->mail_login; + + $email = trim(Minz_Request::param('email', '')); $passwordPlain = Minz_Request::param('newPasswordPlain', '', true); Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP $_POST['newPasswordPlain'] = ''; $apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true); - $ok = self::updateUser(Minz_Session::param('currentUser'), $passwordPlain, $apiPasswordPlain, array( + if ($system_conf->force_email_validation && empty($email)) { + Minz_Request::bad( + _t('user.email.feedback.required'), + array('c' => 'user', 'a' => 'profile') + ); + } + + if (!empty($email) && !validateEmailAddress($email)) { + Minz_Request::bad( + _t('user.email.feedback.invalid'), + array('c' => 'user', 'a' => 'profile') + ); + } + + $ok = self::updateUser( + Minz_Session::param('currentUser'), + $email, + $passwordPlain, + $apiPasswordPlain, + array( 'token' => Minz_Request::param('token', null), - )); + ) + ); Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); if ($ok) { - if ($passwordPlain == '') { + if ($system_conf->force_email_validation && $email !== $old_email) { + Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'validateEmail')); + } elseif ($passwordPlain == '') { Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile')); } else { Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index')); @@ -151,6 +195,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { Minz_View::prependTitle(_t('admin.user.title') . ' · '); + $this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation; $this->view->current_user = Minz_Request::param('u'); $this->view->nb_articles = 0; @@ -165,7 +210,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { } } - public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) { + public static function createUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) { if (!is_array($userConfig)) { $userConfig = array(); } @@ -193,7 +238,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { if ($ok) { $userDAO = new FreshRSS_UserDAO(); $ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds); - $ok &= self::updateUser($new_user_name, $passwordPlain, $apiPasswordPlain); + $ok &= self::updateUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain); } return $ok; } @@ -204,6 +249,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { * Request parameters are: * - new_user_language * - new_user_name + * - new_user_email * - new_user_passwordPlain * - r (i.e. a redirection url, optional) * @@ -216,11 +262,28 @@ class FreshRSS_user_Controller extends Minz_ActionController { } if (Minz_Request::isPost()) { + $system_conf = FreshRSS_Context::$system_conf; + $new_user_name = Minz_Request::param('new_user_name'); + $email = Minz_Request::param('new_user_email', ''); $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true); $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); - $ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language)); + if ($system_conf->force_email_validation && empty($email)) { + Minz_Request::bad( + _t('user.email.feedback.required'), + array('c' => 'auth', 'a' => 'register') + ); + } + + if (!empty($email) && !validateEmailAddress($email)) { + Minz_Request::bad( + _t('user.email.feedback.invalid'), + array('c' => 'auth', 'a' => 'register') + ); + } + + $ok = self::createUser($new_user_name, $email, $passwordPlain, '', array('language' => $new_user_language)); Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP $_POST['new_user_passwordPlain'] = ''; invalidateHttpCache(); @@ -272,6 +335,122 @@ class FreshRSS_user_Controller extends Minz_ActionController { return $ok; } + /** + * This action validates an email address, based on the token sent by email. + * It also serves the main page when user is blocked. + * + * Request parameters are: + * - username + * - token + * + * This route works with GET requests since the URL is provided by email. + * The security risks (e.g. forged URL by an attacker) are not very high so + * it's ok. + * + * It returns 404 error if `force_email_validation` is disabled or if the + * user doesn't exist. + * + * It returns 403 if user isn't logged in and `username` param isn't passed. + */ + public function validateEmailAction() { + if (!FreshRSS_Context::$system_conf->force_email_validation) { + Minz_Error::error(404); + } + + Minz_View::prependTitle(_t('user.email.validation.title') . ' · '); + $this->view->_layout('simple'); + + $username = Minz_Request::param('username'); + $token = Minz_Request::param('token'); + + if ($username) { + $user_config = get_user_configuration($username); + } elseif (FreshRSS_Auth::hasAccess()) { + $user_config = FreshRSS_Context::$user_conf; + } else { + Minz_Error::error(403); + } + + if (!FreshRSS_UserDAO::exists($username) || $user_config === null) { + Minz_Error::error(404); + } + + if ($user_config->email_validation_token === '') { + Minz_Request::good( + _t('user.email.validation.feedback.unnecessary'), + array('c' => 'index', 'a' => 'index') + ); + } + + if ($token) { + if ($user_config->email_validation_token !== $token) { + Minz_Request::bad( + _t('user.email.validation.feedback.wrong_token'), + array('c' => 'user', 'a' => 'validateEmail') + ); + } + + $user_config->email_validation_token = ''; + if ($user_config->save()) { + Minz_Request::good( + _t('user.email.validation.feedback.ok'), + array('c' => 'index', 'a' => 'index') + ); + } else { + Minz_Request::bad( + _t('user.email.validation.feedback.error'), + array('c' => 'user', 'a' => 'validateEmail') + ); + } + } + } + + /** + * This action resends a validation email to the current user. + * + * It only acts on POST requests but doesn't require any param (except the + * CSRF token). + * + * It returns 403 error if the user is not logged in or 404 if request is + * not POST. Else it redirects silently to the index if user has already + * validated its email, or to the user#validateEmail route. + */ + public function sendValidationEmailAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + if (!Minz_Request::isPost()) { + Minz_Error::error(404); + } + + $username = Minz_Session::param('currentUser', '_'); + $user_config = FreshRSS_Context::$user_conf; + + if ($user_config->email_validation_token === '') { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); + } + + $mailer = new FreshRSS_User_Mailer(); + $ok = $mailer->send_email_need_validation($username, $user_config); + + $redirect_url = array('c' => 'user', 'a' => 'validateEmail'); + if ($ok) { + Minz_Request::good( + _t('user.email.validation.feedback.email_sent'), + $redirect_url + ); + } else { + Minz_Request::bad( + _t('user.email.validation.feedback.email_failed'), + $redirect_url + ); + } + } + /** * This action delete an existing user. * diff --git a/app/FreshRSS.php b/app/FreshRSS.php index d578beac4..c48ad2093 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -54,6 +54,8 @@ class FreshRSS extends Minz_FrontController { Minz_ExtensionManager::enableByList($ext_list); } + self::checkEmailValidated(); + Minz_ExtensionManager::callHook('freshrss_init'); } @@ -144,4 +146,20 @@ class FreshRSS extends Minz_FrontController { FreshRSS_Share::load(join_path(APP_PATH, 'shares.php')); self::loadStylesAndScripts(); } + + private static function checkEmailValidated() { + $email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== ''; + $action_is_allowed = ( + Minz_Request::is('user', 'validateEmail') || + Minz_Request::is('user', 'sendValidationEmail') || + Minz_Request::is('user', 'profile') || + Minz_Request::is('auth', 'logout') + ); + if ($email_not_verified && !$action_is_allowed) { + Minz_Request::forward(array( + 'c' => 'user', + 'a' => 'validateEmail', + ), true); + } + } } diff --git a/app/Mailers/UserMailer.php b/app/Mailers/UserMailer.php new file mode 100644 index 000000000..5a2d39f1a --- /dev/null +++ b/app/Mailers/UserMailer.php @@ -0,0 +1,31 @@ +view->_path('user_mailer/email_need_validation.txt'); + + $this->view->username = $username; + $this->view->site_title = FreshRSS_Context::$system_conf->title; + $this->view->validation_url = Minz_Url::display( + array( + 'c' => 'user', + 'a' => 'validateEmail', + 'params' => array( + 'username' => $username, + 'token' => $user_config->email_validation_token + ) + ), + 'txt', + true + ); + + $subject_prefix = '[' . FreshRSS_Context::$system_conf->title . ']'; + return $this->mail( + $user_config->mail_login, + $subject_prefix . ' ' ._t('user.mailer.email_need_validation.title') + ); + } +} diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 778513f17..963d37e2b 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -389,4 +389,8 @@ class FreshRSS_ConfigurationSetter { $data['auto_update_url'] = $value; } + + private function _force_email_validation(&$data, $value) { + $data['force_email_validation'] = $this->handleBool($value); + } } diff --git a/app/i18n/cz/admin.php b/app/i18n/cz/admin.php index 68127f571..a2a509560 100644 --- a/app/i18n/cz/admin.php +++ b/app/i18n/cz/admin.php @@ -163,6 +163,7 @@ return array( 'help' => 'in seconds', //TODO - Translation 'number' => 'Duration to keep logged in', //TODO - Translation ), + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Instance name', //TODO - Translation 'max-categories' => 'Categories per user limit', //TODO - Translation 'max-feeds' => 'Feeds per user limit', //TODO - Translation diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php index 8a21067ee..cd7535571 100644 --- a/app/i18n/cz/conf.php +++ b/app/i18n/cz/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Smazání účtu', 'warn' => 'Váš účet bude smazán spolu se všemi souvisejícími daty', ), + 'email' => 'Email', 'password_api' => 'Password API
(tzn. pro mobilní aplikace)', 'password_form' => 'Heslo
(pro přihlášení webovým formulářem)', 'password_format' => 'Alespoň 7 znaků', diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php index f1a412252..990ff0c30 100644 --- a/app/i18n/cz/gen.php +++ b/app/i18n/cz/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Aktualizovat', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Zpět na seznam RSS kanálů', 'cancel' => 'Zrušit', 'create' => 'Vytvořit', diff --git a/app/i18n/cz/user.php b/app/i18n/cz/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/cz/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php index f0307dcab..d075bf28f 100644 --- a/app/i18n/de/admin.php +++ b/app/i18n/de/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Systemeinstellungen', 'auto-update-url' => 'Auto-update URL', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Dein Reader Name', 'max-categories' => 'Anzahl erlaubter Kategorien pro Benutzer', 'max-feeds' => 'Anzahl erlaubter Feeds pro Benutzer', diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index 37a67eb15..99225da9c 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Accountlöschung', 'warn' => 'Dein Account und alle damit bezogenen Daten werden gelöscht.', ), + 'email' => 'E-Mail-Adresse', 'password_api' => 'Passwort-API
(z. B. für mobile Anwendungen)', 'password_form' => 'Passwort
(für die Anmeldemethode per Webformular)', 'password_format' => 'mindestens 7 Zeichen', diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 93bd62d41..135263d28 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Aktualisieren', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen', 'cancel' => 'Abbrechen', 'create' => 'Erstellen', diff --git a/app/i18n/de/user.php b/app/i18n/de/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/de/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 004089ffc..c5ab183e8 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'System configuration', 'auto-update-url' => 'Auto-update server URL', + 'force_email_validation' => 'Force email addresses validation', 'instance-name' => 'Instance name', 'max-categories' => 'Categories per user limit', 'max-feeds' => 'Feeds per user limit', diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php index 8193233ce..1078c736c 100644 --- a/app/i18n/en/conf.php +++ b/app/i18n/en/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Account deletion', 'warn' => 'Your account and all related data will be deleted.', ), + 'email' => 'Email address', 'password_api' => 'API password
(e.g., for mobile apps)', 'password_form' => 'Password
(for the Web-form login method)', 'password_format' => 'At least 7 characters', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 655b02402..470c27234 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Actualize', + 'back' => '← Go back', 'back_to_rss_feeds' => '← Go back to your RSS feeds', 'cancel' => 'Cancel', 'create' => 'Create', diff --git a/app/i18n/en/user.php b/app/i18n/en/user.php new file mode 100644 index 000000000..5b4cd4fcb --- /dev/null +++ b/app/i18n/en/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', + 'required' => 'The email address is required.', + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', + 'email_sent' => 'An email has been sent to your address.', + 'error' => 'The email address failed to be validated.', + 'ok' => 'The email address has been validated.', + 'unneccessary' => 'The email address was already validated.', + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', + 'resend_email' => 'Resend the email', + 'title' => 'Email address validation', + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', + 'welcome' => 'Welcome %s,', + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', + ), + ), +); diff --git a/app/i18n/es/admin.php b/app/i18n/es/admin.php index 0ec8549bd..1af3bdcb2 100755 --- a/app/i18n/es/admin.php +++ b/app/i18n/es/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Configuración del sistema', 'auto-update-url' => 'URL de auto-actualización', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Nombre de la fuente', 'max-categories' => 'Límite de categorías por usuario', 'max-feeds' => 'Límite de fuentes por usuario', diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php index 2eeeee052..6aaad8d13 100755 --- a/app/i18n/es/conf.php +++ b/app/i18n/es/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Borrar cuenta', 'warn' => 'Tu cuenta y todos los datos asociados serán eliminados.', ), + 'email' => 'Correo electrónico', 'password_api' => 'Contraseña API
(para apps móviles, por ej.)', 'password_form' => 'Contraseña
(para el método de identificación por formulario web)', 'password_format' => 'Mínimo de 7 caracteres', diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php index 16c8267a6..e73aaef12 100755 --- a/app/i18n/es/gen.php +++ b/app/i18n/es/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Actualizar', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← regresar a tus fuentes RSS', 'cancel' => 'Cancelar', 'create' => 'Crear', diff --git a/app/i18n/es/user.php b/app/i18n/es/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/es/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index 74605b5ee..6002617fc 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Configuration du système', 'auto-update-url' => 'URL du service de mise à jour', + 'force_email_validation' => 'Forcer la validation des adresses email', 'instance-name' => 'Nom de l’instance', 'max-categories' => 'Limite de catégories par utilisateur', 'max-feeds' => 'Limite de flux par utilisateur', diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php index 5f6730b53..dcd623b5a 100644 --- a/app/i18n/fr/conf.php +++ b/app/i18n/fr/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Suppression du compte', 'warn' => 'Le compte et toutes les données associées vont être supprimées.', ), + 'email' => 'Adresse email', 'password_api' => 'Mot de passe API
(ex. : pour applis mobiles)', 'password_form' => 'Mot de passe
(pour connexion par formulaire)', 'password_format' => '7 caractères minimum', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index d99e42fca..8e195c60d 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Actualiser', + 'back' => '← Retour', 'back_to_rss_feeds' => '← Retour à vos flux RSS', 'cancel' => 'Annuler', 'create' => 'Créer', diff --git a/app/i18n/fr/user.php b/app/i18n/fr/user.php new file mode 100644 index 000000000..01d3ad1af --- /dev/null +++ b/app/i18n/fr/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'L’adresse email est invalide.', + 'required' => 'L’adresse email est requise.', + ), + 'validation' => array( + 'change_email' => 'Vous pouvez changer votre adresse email dans votre profil.', + 'email_sent_to' => 'Nous venons d’envoyer un email à %s, veuillez suivre ses indications pour valider votre adresse.', + 'feedback' => array( + 'email_failed' => 'Nous n’avons pas pu vous envoyer d’email à cause d’une mauvaise configuration du serveur.', + 'email_sent' => 'Un email a été envoyé à votre adresse.', + 'error' => 'L’adresse email n’a pas pu être validée.', + 'ok' => 'L’adresse email a été validée.', + 'unnecessary' => 'L’adresse email a déjà été validée.', + 'wrong_token' => 'L’adresse email n’a pas pu être validée à cause d’un mauvais token.', + ), + 'need_to' => 'Vous devez valider votre adresse email avant de pouvoir utiliser %s.', + 'resend_email' => 'Renvoyer l’email', + 'title' => 'Validation de l’adresse email', + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'Vous devez valider votre compte', + 'welcome' => 'Bienvenue %s,', + 'body' => 'Vous venez de vous inscrire sur %s mais vous devez encore valider votre adresse email. Pour cela, veuillez cliquer sur ce lien :', + ), + ), +); diff --git a/app/i18n/he/admin.php b/app/i18n/he/admin.php index e0dfc405d..759b74e2a 100644 --- a/app/i18n/he/admin.php +++ b/app/i18n/he/admin.php @@ -163,6 +163,7 @@ return array( 'help' => 'in seconds', //TODO - Translation 'number' => 'Duration to keep logged in', //TODO - Translation ), + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Instance name', //TODO - Translation 'max-categories' => 'Categories per user limit', //TODO - Translation 'max-feeds' => 'Feeds per user limit', //TODO - Translation diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php index 1da5c292c..7e764b944 100644 --- a/app/i18n/he/conf.php +++ b/app/i18n/he/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Account deletion', //TODO - Translation 'warn' => 'Your account and all related data will be deleted.', //TODO - Translation ), + 'email' => 'Email address', //TODO - Translation 'password_api' => 'סיסמת API
(לדוגמה ליישומים סלולריים)', 'password_form' => 'סיסמה
(לשימוש בטפוס ההרשמה)', 'password_format' => 'At least 7 characters', //TODO - Translation diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php index 13084fda0..270a66ea0 100644 --- a/app/i18n/he/gen.php +++ b/app/i18n/he/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'מימוש', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← חזרה להזנות הRSS שלך', 'cancel' => 'ביטול', 'create' => 'יצירה', diff --git a/app/i18n/he/user.php b/app/i18n/he/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/he/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/it/admin.php b/app/i18n/it/admin.php index d4253e9ba..8bb6c7bfe 100644 --- a/app/i18n/it/admin.php +++ b/app/i18n/it/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Configurazione di sistema', 'auto-update-url' => 'Auto-update server URL', //TODO - Translation + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Nome istanza', 'max-categories' => 'Limite categorie per utente', 'max-feeds' => 'Limite feeds per utente', diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php index f3c59ed8c..f06302c72 100644 --- a/app/i18n/it/conf.php +++ b/app/i18n/it/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Cancellazione account', 'warn' => 'Il tuo account e tutti i dati associati saranno cancellati.', ), + 'email' => 'Indirizzo email', 'password_api' => 'Password API
(e.g., per applicazioni mobili)', 'password_form' => 'Password
(per il login classico)', 'password_format' => 'Almeno 7 caratteri', diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php index 4f6f7e36f..e5114439c 100644 --- a/app/i18n/it/gen.php +++ b/app/i18n/it/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Aggiorna', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Indietro', 'cancel' => 'Annulla', 'create' => 'Crea', diff --git a/app/i18n/it/user.php b/app/i18n/it/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/it/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/kr/admin.php b/app/i18n/kr/admin.php index 6312bd3fe..4a8e425d5 100644 --- a/app/i18n/kr/admin.php +++ b/app/i18n/kr/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => '시스템 설정', 'auto-update-url' => '자동 업데이트 서버 URL', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => '인스턴스 이름', 'max-categories' => '사용자별 카테고리 개수 제한', 'max-feeds' => '사용자별 피드 개수 제한', diff --git a/app/i18n/kr/conf.php b/app/i18n/kr/conf.php index 1efaee88b..397d57418 100644 --- a/app/i18n/kr/conf.php +++ b/app/i18n/kr/conf.php @@ -46,6 +46,7 @@ return array( '_' => '계정 삭제', 'warn' => '당신의 계정과 관련된 모든 데이터가 삭제됩니다.', ), + 'email' => '메일 주소', 'password_api' => 'API 암호
(예: 모바일 애플리케이션)', 'password_form' => '암호
(웹폼 로그인 방식 사용시)', 'password_format' => '7 글자 이상이어야 합니다', diff --git a/app/i18n/kr/gen.php b/app/i18n/kr/gen.php index 72b95fa6e..979cb9ffa 100644 --- a/app/i18n/kr/gen.php +++ b/app/i18n/kr/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => '새 글 가져오기', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← RSS 피드로 돌아가기', 'cancel' => '취소', 'create' => '생성', diff --git a/app/i18n/kr/user.php b/app/i18n/kr/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/kr/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php index e5d126eb8..535241e58 100644 --- a/app/i18n/nl/admin.php +++ b/app/i18n/nl/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Systeem configuratie', 'auto-update-url' => 'Automatische update server URL', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Voorbeeld naam', 'max-categories' => 'Categorielimiet per gebruiker', 'max-feeds' => 'Feedlimiet per gebruiker', diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php index b7ba7bbeb..ec219d051 100644 --- a/app/i18n/nl/conf.php +++ b/app/i18n/nl/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Account verwijderen', 'warn' => 'Uw account en alle gerelateerde gegvens worden verwijderd.', ), + 'email' => 'Email adres', 'password_api' => 'Wachtwoord API
(e.g., voor mobiele apps)', 'password_form' => 'Wachtwoord
(voor de Web-formulier log in methode)', 'password_format' => 'Ten minste 7 tekens', diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php index 57d68d673..419d8b36c 100644 --- a/app/i18n/nl/gen.php +++ b/app/i18n/nl/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Actualiseren', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Ga terug naar je RSS feeds', 'cancel' => 'Annuleren', 'create' => 'Opslaan', diff --git a/app/i18n/nl/user.php b/app/i18n/nl/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/nl/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/oc/admin.php b/app/i18n/oc/admin.php index 2f8ede873..aee12a20d 100644 --- a/app/i18n/oc/admin.php +++ b/app/i18n/oc/admin.php @@ -163,6 +163,7 @@ return array( 'help' => 'en segondas', 'number' => 'Durada de téner d’ésser connectat', ), + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Nom de l’instància', 'max-categories' => 'Limita de categoria per utilizaire', 'max-feeds' => 'Limita de fluxes per utilizaire', diff --git a/app/i18n/oc/conf.php b/app/i18n/oc/conf.php index b37785a7e..19880b3f8 100644 --- a/app/i18n/oc/conf.php +++ b/app/i18n/oc/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Supression del compte', 'warn' => 'Lo compte e totas las donadas ligadas seràn suprimits.', ), + 'email' => 'Adreça de corrièl', 'password_api' => 'Senhal API
(ex. : per las aplicacions mobil)', 'password_form' => 'Senhal API
(ex. : per la connexion via formulari)', 'password_format' => 'Almens 7 caractèrs', diff --git a/app/i18n/oc/gen.php b/app/i18n/oc/gen.php index 1fa1358ff..281bb06f1 100644 --- a/app/i18n/oc/gen.php +++ b/app/i18n/oc/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Actualizar', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Tornar a vòstres fluxes RSS', 'cancel' => 'Anullar', 'create' => 'Crear', diff --git a/app/i18n/oc/user.php b/app/i18n/oc/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/oc/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/pt-br/admin.php b/app/i18n/pt-br/admin.php index 82559c67b..cef6694c2 100644 --- a/app/i18n/pt-br/admin.php +++ b/app/i18n/pt-br/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Configuração do sistema', 'auto-update-url' => 'URL do servidor para atualização automática', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Nome da instância', 'max-categories' => 'Limite de categorias por usuário', 'max-feeds' => 'Limite de Feeds por usuário', diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php index 082027328..eb067e58a 100644 --- a/app/i18n/pt-br/conf.php +++ b/app/i18n/pt-br/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Remover conta', 'warn' => 'Sua conta e todos os dados relacionados serão removidos.', ), + 'email' => 'Endereço de e-mail', 'password_api' => 'Senha da API
(p.s., para aplicativos móveis)', 'password_form' => 'Senha
(para o método de formulário web)', 'password_format' => 'Ao menos 7 caracteres', diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php index 2d2b5f1a4..e536e1d95 100644 --- a/app/i18n/pt-br/gen.php +++ b/app/i18n/pt-br/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Atualizar', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Volte para o seu feeds RSS', 'cancel' => 'Cancelar', 'create' => 'Criar', diff --git a/app/i18n/pt-br/user.php b/app/i18n/pt-br/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/pt-br/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/ru/admin.php b/app/i18n/ru/admin.php index c9a7d6683..adf091df9 100644 --- a/app/i18n/ru/admin.php +++ b/app/i18n/ru/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Системные настройки', 'auto-update-url' => 'Адрес сервера для автоматического обновления', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Название этого сервера', 'max-categories' => 'Количество категорий на пользователя', 'max-feeds' => 'Количество статей на пользователя', diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php index 48ce4b9f3..af6f3b5f6 100644 --- a/app/i18n/ru/conf.php +++ b/app/i18n/ru/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Account deletion', //TODO - Translation 'warn' => 'Your account and all the related data will be deleted.', //TODO - Translation ), + 'email' => 'Email address', //TODO - Translation 'password_api' => 'Password API
(e.g., for mobile apps)', //TODO - Translation 'password_form' => 'Password
(for the Web-form login method)', //TODO - Translation 'password_format' => 'At least 7 characters', //TODO - Translation diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php index f28e5adad..39f3c7534 100644 --- a/app/i18n/ru/gen.php +++ b/app/i18n/ru/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Actualize', //TODO - Translation + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← Go back to your RSS feeds', //TODO - Translation 'cancel' => 'Cancel', //TODO - Translation 'create' => 'Create', //TODO - Translation diff --git a/app/i18n/ru/user.php b/app/i18n/ru/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/ru/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/tr/admin.php b/app/i18n/tr/admin.php index b1d6671ca..2c7d0fd6d 100644 --- a/app/i18n/tr/admin.php +++ b/app/i18n/tr/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => 'Sistem yapılandırması', 'auto-update-url' => 'Otomatik güncelleme sunucu URL', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => 'Örnek isim', 'max-categories' => 'Kullanıcı başına kategori limiti', 'max-feeds' => 'Kullanıcı başına akış limiti', diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php index 855bca6c8..2bf1e8a6a 100644 --- a/app/i18n/tr/conf.php +++ b/app/i18n/tr/conf.php @@ -46,6 +46,7 @@ return array( '_' => 'Hesap silme', 'warn' => 'Hesabınız ve tüm verileriniz silinecek.', ), + 'email' => 'Email adresleri', 'password_api' => 'API Şifresi
(ör. mobil uygulamalar için)', 'password_form' => 'Şifre
(Tarayıcı girişi için)', 'password_format' => 'En az 7 karakter', diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php index e167f5a09..8bac03746 100644 --- a/app/i18n/tr/gen.php +++ b/app/i18n/tr/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => 'Yenile', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← RSS akışlarınız için geri gidin', 'cancel' => 'İptal', 'create' => 'Oluştur', diff --git a/app/i18n/tr/user.php b/app/i18n/tr/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/tr/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/i18n/zh-cn/admin.php b/app/i18n/zh-cn/admin.php index 74f57b6e8..cdc8449a3 100644 --- a/app/i18n/zh-cn/admin.php +++ b/app/i18n/zh-cn/admin.php @@ -159,6 +159,7 @@ return array( 'system' => array( '_' => '系统配置', 'auto-update-url' => '自动升级服务器 URL', + 'force_email_validation' => 'Force email addresses validation', //TODO - Translation 'instance-name' => '实例名称', 'max-categories' => '每用户分类限制', 'max-feeds' => '每用户 RSS 源限制', diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php index ebe069c2c..2960cd6b1 100644 --- a/app/i18n/zh-cn/conf.php +++ b/app/i18n/zh-cn/conf.php @@ -46,6 +46,7 @@ return array( '_' => '账户删除', 'warn' => '你的帐户和所有相关数据都将被删除。', ), + 'email' => 'Email 地址', 'password_api' => 'API 密码
(例如,用于手机 APP)', 'password_form' => '密码
(用于 Web-form 登录方式)', 'password_format' => '至少 7 个字符', diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php index bef5744b6..4d4c52ed6 100644 --- a/app/i18n/zh-cn/gen.php +++ b/app/i18n/zh-cn/gen.php @@ -3,6 +3,7 @@ return array( 'action' => array( 'actualize' => '获取', + 'back' => '← Go back', //TODO - Translation 'back_to_rss_feeds' => '← 返回', 'cancel' => '取消', 'create' => '创建', diff --git a/app/i18n/zh-cn/user.php b/app/i18n/zh-cn/user.php new file mode 100644 index 000000000..4f2cfcda2 --- /dev/null +++ b/app/i18n/zh-cn/user.php @@ -0,0 +1,32 @@ + array( + 'feedback' => array( + 'invalid' => 'The email address is invalid.', //TODO - Translation + 'required' => 'The email address is required.', //TODO - Translation + ), + 'validation' => array( + 'change_email' => 'You can change your email address on the profile page.', //TODO - Translation + 'email_sent_to' => 'We sent you an email at %s, please follow its indications to validate your address.', //TODO - Translation + 'feedback' => array( + 'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation + 'email_sent' => 'An email has been sent to your address.', //TODO - Translation + 'error' => 'The email address failed to be validated.', //TODO - Translation + 'ok' => 'The email address has been validated.', //TODO - Translation + 'unneccessary' => 'The email address was already validated.', //TODO - Translation + 'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation + ), + 'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation + 'resend_email' => 'Resend the email', //TODO - Translation + 'title' => 'Email address validation', //TODO - Translation + ), + ), + 'mailer' => array( + 'email_need_validation' => array( + 'title' => 'You need to validate your account', //TODO - Translation + 'welcome' => 'Welcome %s,', //TODO - Translation + 'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation + ), + ), +); diff --git a/app/layout/simple.phtml b/app/layout/simple.phtml new file mode 100644 index 000000000..5546966be --- /dev/null +++ b/app/layout/simple.phtml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + + + + () + + +
+
+ + render(); ?> +
+ +notification)) { + $msg = $this->notification['content']; + $status = $this->notification['type']; + + invalidateHttpCache(); + } +?> +
+ + +
+ + + diff --git a/app/views/auth/register.phtml b/app/views/auth/register.phtml index 19e11ef76..87582a2d0 100644 --- a/app/views/auth/register.phtml +++ b/app/views/auth/register.phtml @@ -8,6 +8,15 @@ + show_email_field) { ?> +
+ + +
+ +
diff --git a/app/views/configure/system.phtml b/app/views/configure/system.phtml index 9af4cc2c9..eb0e68dfc 100644 --- a/app/views/configure/system.phtml +++ b/app/views/configure/system.phtml @@ -38,6 +38,24 @@
+ can_enable_email_validation) { ?> +
+
+ +
+
+ +
@@ -51,7 +69,7 @@
- +
diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml index d0e5928ef..501257e5b 100644 --- a/app/views/user/manage.phtml +++ b/app/views/user/manage.phtml @@ -26,6 +26,17 @@
+ show_email_field) { ?> +
+ +
+ +
+
+ +
diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml index 83140376d..87aa25b11 100644 --- a/app/views/user/profile.phtml +++ b/app/views/user/profile.phtml @@ -1,4 +1,8 @@ -partial('aside_configure'); ?> +disable_aside) { + $this->partial('aside_configure'); + } +?>
@@ -18,6 +22,13 @@
+
+ +
+ +
+
+
diff --git a/app/views/user/validateEmail.phtml b/app/views/user/validateEmail.phtml new file mode 100644 index 000000000..a246c222e --- /dev/null +++ b/app/views/user/validateEmail.phtml @@ -0,0 +1,22 @@ +
+

+ title); ?> +

+ +

+ mail_login); ?> +

+ +
+ + +
+ +

+ + + +

+
diff --git a/app/views/user_mailer/email_need_validation.txt b/app/views/user_mailer/email_need_validation.txt new file mode 100644 index 000000000..13b63c1af --- /dev/null +++ b/app/views/user_mailer/email_need_validation.txt @@ -0,0 +1,5 @@ +username); ?> + +site_title); ?> + +validation_url; ?> diff --git a/cli/create-user.php b/cli/create-user.php index 29675fa53..7e0a031d9 100755 --- a/cli/create-user.php +++ b/cli/create-user.php @@ -16,11 +16,14 @@ if (preg_grep("/^$username$/i", $usernames)) { echo 'FreshRSS creating user “', $username, "”…\n"; -$ok = FreshRSS_user_Controller::createUser($username, +$ok = FreshRSS_user_Controller::createUser( + $username, + empty($options['mail_login']) ? '' : $options['mail_login'], empty($options['password']) ? '' : $options['password'], empty($options['api_password']) ? '' : $options['api_password'], $values, - !isset($options['no_default_feeds'])); + !isset($options['no_default_feeds']) +); if (!$ok) { fail('FreshRSS could not create user!'); diff --git a/cli/update-user.php b/cli/update-user.php index 7eb3e81ff..8067dadd3 100755 --- a/cli/update-user.php +++ b/cli/update-user.php @@ -9,6 +9,7 @@ echo 'FreshRSS updating user “', $username, "”…\n"; $ok = FreshRSS_user_Controller::updateUser( $username, + empty($options['mail_login']) ? null : $options['mail_login'], empty($options['password']) ? '' : $options['password'], empty($options['api_password']) ? '' : $options['api_password'], $values); diff --git a/config-user.default.php b/config-user.default.php index 077ea70d9..d7149778d 100644 --- a/config-user.default.php +++ b/config-user.default.php @@ -6,6 +6,7 @@ return array ( 'keep_history_default' => 50, 'ttl_default' => 3600, 'mail_login' => '', + 'email_validation_token' => '', 'token' => '', 'passwordHash' => '', 'apiPasswordHash' => '', diff --git a/config.default.php b/config.default.php index 4c2ffa849..9d74940c1 100644 --- a/config.default.php +++ b/config.default.php @@ -33,6 +33,13 @@ return array( # Name of the user that has administration rights. 'default_user' => '_', + # Force users to validate their email address. If `true`, an email with a + # validation URL is sent during registration, and users cannot access their + # feed if they didn't access this URL. + # Note: it is recommended to not enable it with PHP < 5.5 (emails cannot be + # sent). + 'force_email_validation' => false, + # Allow or not visitors without login to see the articles # of the default user. 'allow_anonymous' => false, @@ -159,7 +166,7 @@ return array( 'username' => '', 'password' => '', 'secure' => '', // '', 'ssl' or 'tls' - 'from' => 'noreply@localhost', + 'from' => 'root@localhost', ), # List of enabled FreshRSS extensions. diff --git a/docs/en/admins/01_Index.md b/docs/en/admins/01_Index.md index 45ed02c0f..443bf2ca9 100644 --- a/docs/en/admins/01_Index.md +++ b/docs/en/admins/01_Index.md @@ -5,5 +5,6 @@ Learn how to install, update and backup FreshRSS and how to use the command line * [Install FreshRSS](02_Installation.md) on your server * [Update your installation](03_Updating.md) to the latest stable or dev version * [The command line interface](https://github.com/FreshRSS/FreshRSS/tree/master/cli) can be used to administrate feeds and users -* [Automatic feed updates](https://github.com/FreshRSS/FreshRSS#automatic-feed-update) using cron is the preferred way to get the latest feeds entries +* [Automatic feed updates](https://github.com/FreshRSS/FreshRSS#automatic-feed-update) using cron is the preferred way to get the latest feeds entries +* [Configuring the email address validation](05_Configuring_email_validation.md) * [Frequently asked questions](04_Frequently_Asked_Questions.md) diff --git a/docs/en/admins/05_Configuring_email_validation.md b/docs/en/admins/05_Configuring_email_validation.md new file mode 100644 index 000000000..6cc9ca8f5 --- /dev/null +++ b/docs/en/admins/05_Configuring_email_validation.md @@ -0,0 +1,73 @@ +# Configuring the email address validation + +FreshRSS can verify that users give a valid email address. It is not configured +by default so you'll have to follow these few steps to verify email addresses. + +It is intended to administrators who host users and want to be sure to be able +to contact them. + +Note that this feature only works with PHP >= 5.5. + +## Force email validation + +In your `data/config.php` file, you'll find a `force_email_validation` item: +set it to `true`. An email field now appears on the registration page and +emails are sent when users change their email. + +You can also enable this feature directly in FreshRSS: `Administration` > +`System configuration` > check `Force email addresses validation`. If the +option doesn't appear, it means that you use PHP < 5.5. + +## Configure the SMTP server + +By default, FreshRSS will attempt to send emails with the [`mail`](https://www.php.net/manual/en/function.mail.php) +function of PHP. It is the simpler solution but it might not work as expected. +For example, we don't support (yet?) sending emails from inside our official +Docker images. We recommend to use a proper SMTP server. + +To configure a SMTP server, you'll have to modify the `data/config.php` file. + +First, change the `mailer` item to `smtp` (instead of the default `mail`). + +Then, you should change the `smtp` options like you would do with a regular +email client. You can find the full list of options in the [`config.default.php` file](/config.default.php). +If you're not sure to what each item is corresponding, you may find useful [the +PHPMailer documentation](http://phpmailer.github.io/PHPMailer/classes/PHPMailer.PHPMailer.PHPMailer.html#properties) +(which is used by FreshRSS under the hood). + +## Check your SMTP server is correctly configured + +To do so, once you've enabled the `force_email_validation` option, you only +need to change your email address on the profile page and check that an email +arrives on the new address. + +If it fails, you can change the environment (in `data/config.php` file, change +`production` to `development`). PHPMailer will become more verbose and you'll +be able to see what happens in the PHP logs. If something's wrong here, you'll +probably better served by asking to your favorite search engine than asking us. +If you think that something's wrong in FreshRSS code, don't hesitate to open a +ticket though. + +Also, make sure the email didn't arrive in your spam. + +Once you're done, don't forget to reconfigure your environment to `production`. + +## Access the validation URL during development + +You might find painful to configure a SMTP server when you're developping and +`mail` function will not work on your local machine. For the moment, there is +no easy way to access the validation URL unless forging it. You'll need to +information: + +- the username of the user to validate (you should know it) +- its validation token, that you'll find in its configuration file: + +```console +$ # For instance, for a user called `alice` +$ grep email_validation_token data/users/alice/config.php | cut -d \' -f 4 - +3d75042a4471994a0346e18ae87602f19220a795 +``` + +Then, the validation URL should be `http://localhost:8080/i/?c=user&a=validateEmail&username=alice&token=3d75042a4471994a0346e18ae87602f19220a795` + +Don't forget to adapt this URL with the correct port, username and token. diff --git a/lib/Minz/Mailer.php b/lib/Minz/Mailer.php index 0e88f71d9..04392982b 100644 --- a/lib/Minz/Mailer.php +++ b/lib/Minz/Mailer.php @@ -3,10 +3,6 @@ use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception; -require LIB_PATH . '/PHPMailer/PHPMailer.php'; -require LIB_PATH . '/PHPMailer/Exception.php'; -require LIB_PATH . '/PHPMailer/SMTP.php'; - /** * Allow to send emails. * @@ -78,6 +74,8 @@ class Minz_Mailer { $body = ob_get_contents(); ob_end_clean(); + PHPMailer::$validator = 'html5'; + $mail = new PHPMailer(true); try { // Server settings diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 912c354ac..01feece52 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -98,6 +98,13 @@ class Minz_Request { self::initJSON(); } + public static function is($controller_name, $action_name) { + return ( + self::$controller_name === $controller_name && + self::$action_name === $action_name + ); + } + /** * Return true if the request is over HTTPS, false otherwise (HTTP) */ diff --git a/lib/PHPMailer/Exception.php b/lib/PHPMailer/Exception.php deleted file mode 100644 index 9a05dec3c..000000000 --- a/lib/PHPMailer/Exception.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @author Jim Jagielski (jimjag) - * @author Andy Prevost (codeworxtech) - * @author Brent R. Matzelle (original founder) - * @copyright 2012 - 2017 Marcus Bointon - * @copyright 2010 - 2012 Jim Jagielski - * @copyright 2004 - 2009 Andy Prevost - * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License - * @note This program is distributed in the hope that it will be useful - WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. - */ - -namespace PHPMailer\PHPMailer; - -/** - * PHPMailer exception handler. - * - * @author Marcus Bointon - */ -class Exception extends \Exception -{ - /** - * Prettify error message output. - * - * @return string - */ - public function errorMessage() - { - return '' . htmlspecialchars($this->getMessage()) . "
\n"; - } -} diff --git a/lib/PHPMailer/PHPMailer.php b/lib/PHPMailer/PHPMailer.php deleted file mode 100644 index 52104924d..000000000 --- a/lib/PHPMailer/PHPMailer.php +++ /dev/null @@ -1,4502 +0,0 @@ - - * @author Jim Jagielski (jimjag) - * @author Andy Prevost (codeworxtech) - * @author Brent R. Matzelle (original founder) - * @copyright 2012 - 2017 Marcus Bointon - * @copyright 2010 - 2012 Jim Jagielski - * @copyright 2004 - 2009 Andy Prevost - * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License - * @note This program is distributed in the hope that it will be useful - WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. - */ - -namespace PHPMailer\PHPMailer; - -/** - * PHPMailer - PHP email creation and transport class. - * - * @author Marcus Bointon (Synchro/coolbru) - * @author Jim Jagielski (jimjag) - * @author Andy Prevost (codeworxtech) - * @author Brent R. Matzelle (original founder) - */ -class PHPMailer -{ - const CHARSET_ISO88591 = 'iso-8859-1'; - const CHARSET_UTF8 = 'utf-8'; - - const CONTENT_TYPE_PLAINTEXT = 'text/plain'; - const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; - const CONTENT_TYPE_TEXT_HTML = 'text/html'; - const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; - const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; - const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; - - const ENCODING_7BIT = '7bit'; - const ENCODING_8BIT = '8bit'; - const ENCODING_BASE64 = 'base64'; - const ENCODING_BINARY = 'binary'; - const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; - - /** - * Email priority. - * Options: null (default), 1 = High, 3 = Normal, 5 = low. - * When null, the header is not set at all. - * - * @var int - */ - public $Priority; - - /** - * The character set of the message. - * - * @var string - */ - public $CharSet = self::CHARSET_ISO88591; - - /** - * The MIME Content-type of the message. - * - * @var string - */ - public $ContentType = self::CONTENT_TYPE_PLAINTEXT; - - /** - * The message encoding. - * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". - * - * @var string - */ - public $Encoding = self::ENCODING_8BIT; - - /** - * Holds the most recent mailer error message. - * - * @var string - */ - public $ErrorInfo = ''; - - /** - * The From email address for the message. - * - * @var string - */ - public $From = 'root@localhost'; - - /** - * The From name of the message. - * - * @var string - */ - public $FromName = 'Root User'; - - /** - * The envelope sender of the message. - * This will usually be turned into a Return-Path header by the receiver, - * and is the address that bounces will be sent to. - * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP. - * - * @var string - */ - public $Sender = ''; - - /** - * The Subject of the message. - * - * @var string - */ - public $Subject = ''; - - /** - * An HTML or plain text message body. - * If HTML then call isHTML(true). - * - * @var string - */ - public $Body = ''; - - /** - * The plain-text message body. - * This body can be read by mail clients that do not have HTML email - * capability such as mutt & Eudora. - * Clients that can read HTML will view the normal Body. - * - * @var string - */ - public $AltBody = ''; - - /** - * An iCal message part body. - * Only supported in simple alt or alt_inline message types - * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator. - * - * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ - * @see http://kigkonsult.se/iCalcreator/ - * - * @var string - */ - public $Ical = ''; - - /** - * The complete compiled MIME message body. - * - * @var string - */ - protected $MIMEBody = ''; - - /** - * The complete compiled MIME message headers. - * - * @var string - */ - protected $MIMEHeader = ''; - - /** - * Extra headers that createHeader() doesn't fold in. - * - * @var string - */ - protected $mailHeader = ''; - - /** - * Word-wrap the message body to this number of chars. - * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. - * - * @see static::STD_LINE_LENGTH - * - * @var int - */ - public $WordWrap = 0; - - /** - * Which method to use to send mail. - * Options: "mail", "sendmail", or "smtp". - * - * @var string - */ - public $Mailer = 'mail'; - - /** - * The path to the sendmail program. - * - * @var string - */ - public $Sendmail = '/usr/sbin/sendmail'; - - /** - * Whether mail() uses a fully sendmail-compatible MTA. - * One which supports sendmail's "-oi -f" options. - * - * @var bool - */ - public $UseSendmailOptions = true; - - /** - * The email address that a reading confirmation should be sent to, also known as read receipt. - * - * @var string - */ - public $ConfirmReadingTo = ''; - - /** - * The hostname to use in the Message-ID header and as default HELO string. - * If empty, PHPMailer attempts to find one with, in order, - * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value - * 'localhost.localdomain'. - * - * @var string - */ - public $Hostname = ''; - - /** - * An ID to be used in the Message-ID header. - * If empty, a unique id will be generated. - * You can set your own, but it must be in the format "", - * as defined in RFC5322 section 3.6.4 or it will be ignored. - * - * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 - * - * @var string - */ - public $MessageID = ''; - - /** - * The message Date to be used in the Date header. - * If empty, the current date will be added. - * - * @var string - */ - public $MessageDate = ''; - - /** - * SMTP hosts. - * Either a single hostname or multiple semicolon-delimited hostnames. - * You can also specify a different port - * for each host by using this format: [hostname:port] - * (e.g. "smtp1.example.com:25;smtp2.example.com"). - * You can also specify encryption type, for example: - * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). - * Hosts will be tried in order. - * - * @var string - */ - public $Host = 'localhost'; - - /** - * The default SMTP server port. - * - * @var int - */ - public $Port = 25; - - /** - * The SMTP HELO of the message. - * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find - * one with the same method described above for $Hostname. - * - * @see PHPMailer::$Hostname - * - * @var string - */ - public $Helo = ''; - - /** - * What kind of encryption to use on the SMTP connection. - * Options: '', 'ssl' or 'tls'. - * - * @var string - */ - public $SMTPSecure = ''; - - /** - * Whether to enable TLS encryption automatically if a server supports it, - * even if `SMTPSecure` is not set to 'tls'. - * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. - * - * @var bool - */ - public $SMTPAutoTLS = true; - - /** - * Whether to use SMTP authentication. - * Uses the Username and Password properties. - * - * @see PHPMailer::$Username - * @see PHPMailer::$Password - * - * @var bool - */ - public $SMTPAuth = false; - - /** - * Options array passed to stream_context_create when connecting via SMTP. - * - * @var array - */ - public $SMTPOptions = []; - - /** - * SMTP username. - * - * @var string - */ - public $Username = ''; - - /** - * SMTP password. - * - * @var string - */ - public $Password = ''; - - /** - * SMTP auth type. - * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified. - * - * @var string - */ - public $AuthType = ''; - - /** - * An instance of the PHPMailer OAuth class. - * - * @var OAuth - */ - protected $oauth; - - /** - * The SMTP server timeout in seconds. - * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. - * - * @var int - */ - public $Timeout = 300; - - /** - * SMTP class debug output mode. - * Debug output level. - * Options: - * * `0` No output - * * `1` Commands - * * `2` Data and commands - * * `3` As 2 plus connection status - * * `4` Low-level data output. - * - * @see SMTP::$do_debug - * - * @var int - */ - public $SMTPDebug = 0; - - /** - * How to handle debug output. - * Options: - * * `echo` Output plain-text as-is, appropriate for CLI - * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output - * * `error_log` Output to error log as configured in php.ini - * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. - * Alternatively, you can provide a callable expecting two params: a message string and the debug level: - * - * ```php - * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; - * ``` - * - * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` - * level output is used: - * - * ```php - * $mail->Debugoutput = new myPsr3Logger; - * ``` - * - * @see SMTP::$Debugoutput - * - * @var string|callable|\Psr\Log\LoggerInterface - */ - public $Debugoutput = 'echo'; - - /** - * Whether to keep SMTP connection open after each message. - * If this is set to true then to close the connection - * requires an explicit call to smtpClose(). - * - * @var bool - */ - public $SMTPKeepAlive = false; - - /** - * Whether to split multiple to addresses into multiple messages - * or send them all in one message. - * Only supported in `mail` and `sendmail` transports, not in SMTP. - * - * @var bool - */ - public $SingleTo = false; - - /** - * Storage for addresses when SingleTo is enabled. - * - * @var array - */ - protected $SingleToArray = []; - - /** - * Whether to generate VERP addresses on send. - * Only applicable when sending via SMTP. - * - * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path - * @see http://www.postfix.org/VERP_README.html Postfix VERP info - * - * @var bool - */ - public $do_verp = false; - - /** - * Whether to allow sending messages with an empty body. - * - * @var bool - */ - public $AllowEmpty = false; - - /** - * DKIM selector. - * - * @var string - */ - public $DKIM_selector = ''; - - /** - * DKIM Identity. - * Usually the email address used as the source of the email. - * - * @var string - */ - public $DKIM_identity = ''; - - /** - * DKIM passphrase. - * Used if your key is encrypted. - * - * @var string - */ - public $DKIM_passphrase = ''; - - /** - * DKIM signing domain name. - * - * @example 'example.com' - * - * @var string - */ - public $DKIM_domain = ''; - - /** - * DKIM Copy header field values for diagnostic use. - * - * @var bool - */ - public $DKIM_copyHeaderFields = true; - - /** - * DKIM Extra signing headers. - * - * @example ['List-Unsubscribe', 'List-Help'] - * - * @var array - */ - public $DKIM_extraHeaders = []; - - /** - * DKIM private key file path. - * - * @var string - */ - public $DKIM_private = ''; - - /** - * DKIM private key string. - * - * If set, takes precedence over `$DKIM_private`. - * - * @var string - */ - public $DKIM_private_string = ''; - - /** - * Callback Action function name. - * - * The function that handles the result of the send email action. - * It is called out by send() for each email sent. - * - * Value can be any php callable: http://www.php.net/is_callable - * - * Parameters: - * bool $result result of the send action - * array $to email addresses of the recipients - * array $cc cc email addresses - * array $bcc bcc email addresses - * string $subject the subject - * string $body the email body - * string $from email address of sender - * string $extra extra information of possible use - * "smtp_transaction_id' => last smtp transaction id - * - * @var string - */ - public $action_function = ''; - - /** - * What to put in the X-Mailer header. - * Options: An empty string for PHPMailer default, whitespace for none, or a string to use. - * - * @var string - */ - public $XMailer = ''; - - /** - * Which validator to use by default when validating email addresses. - * May be a callable to inject your own validator, but there are several built-in validators. - * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. - * - * @see PHPMailer::validateAddress() - * - * @var string|callable - */ - public static $validator = 'php'; - - /** - * An instance of the SMTP sender class. - * - * @var SMTP - */ - protected $smtp; - - /** - * The array of 'to' names and addresses. - * - * @var array - */ - protected $to = []; - - /** - * The array of 'cc' names and addresses. - * - * @var array - */ - protected $cc = []; - - /** - * The array of 'bcc' names and addresses. - * - * @var array - */ - protected $bcc = []; - - /** - * The array of reply-to names and addresses. - * - * @var array - */ - protected $ReplyTo = []; - - /** - * An array of all kinds of addresses. - * Includes all of $to, $cc, $bcc. - * - * @see PHPMailer::$to - * @see PHPMailer::$cc - * @see PHPMailer::$bcc - * - * @var array - */ - protected $all_recipients = []; - - /** - * An array of names and addresses queued for validation. - * In send(), valid and non duplicate entries are moved to $all_recipients - * and one of $to, $cc, or $bcc. - * This array is used only for addresses with IDN. - * - * @see PHPMailer::$to - * @see PHPMailer::$cc - * @see PHPMailer::$bcc - * @see PHPMailer::$all_recipients - * - * @var array - */ - protected $RecipientsQueue = []; - - /** - * An array of reply-to names and addresses queued for validation. - * In send(), valid and non duplicate entries are moved to $ReplyTo. - * This array is used only for addresses with IDN. - * - * @see PHPMailer::$ReplyTo - * - * @var array - */ - protected $ReplyToQueue = []; - - /** - * The array of attachments. - * - * @var array - */ - protected $attachment = []; - - /** - * The array of custom headers. - * - * @var array - */ - protected $CustomHeader = []; - - /** - * The most recent Message-ID (including angular brackets). - * - * @var string - */ - protected $lastMessageID = ''; - - /** - * The message's MIME type. - * - * @var string - */ - protected $message_type = ''; - - /** - * The array of MIME boundary strings. - * - * @var array - */ - protected $boundary = []; - - /** - * The array of available languages. - * - * @var array - */ - protected $language = []; - - /** - * The number of errors encountered. - * - * @var int - */ - protected $error_count = 0; - - /** - * The S/MIME certificate file path. - * - * @var string - */ - protected $sign_cert_file = ''; - - /** - * The S/MIME key file path. - * - * @var string - */ - protected $sign_key_file = ''; - - /** - * The optional S/MIME extra certificates ("CA Chain") file path. - * - * @var string - */ - protected $sign_extracerts_file = ''; - - /** - * The S/MIME password for the key. - * Used only if the key is encrypted. - * - * @var string - */ - protected $sign_key_pass = ''; - - /** - * Whether to throw exceptions for errors. - * - * @var bool - */ - protected $exceptions = false; - - /** - * Unique ID used for message ID and boundaries. - * - * @var string - */ - protected $uniqueid = ''; - - /** - * The PHPMailer Version number. - * - * @var string - */ - const VERSION = '6.0.7'; - - /** - * Error severity: message only, continue processing. - * - * @var int - */ - const STOP_MESSAGE = 0; - - /** - * Error severity: message, likely ok to continue processing. - * - * @var int - */ - const STOP_CONTINUE = 1; - - /** - * Error severity: message, plus full stop, critical error reached. - * - * @var int - */ - const STOP_CRITICAL = 2; - - /** - * SMTP RFC standard line ending. - * - * @var string - */ - protected static $LE = "\r\n"; - - /** - * The maximum line length allowed by RFC 2822 section 2.1.1. - * - * @var int - */ - const MAX_LINE_LENGTH = 998; - - /** - * The lower maximum line length allowed by RFC 2822 section 2.1.1. - * This length does NOT include the line break - * 76 means that lines will be 77 or 78 chars depending on whether - * the line break format is LF or CRLF; both are valid. - * - * @var int - */ - const STD_LINE_LENGTH = 76; - - /** - * Constructor. - * - * @param bool $exceptions Should we throw external exceptions? - */ - public function __construct($exceptions = null) - { - if (null !== $exceptions) { - $this->exceptions = (bool) $exceptions; - } - //Pick an appropriate debug output format automatically - $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); - } - - /** - * Destructor. - */ - public function __destruct() - { - //Close any open SMTP connection nicely - $this->smtpClose(); - } - - /** - * Call mail() in a safe_mode-aware fashion. - * Also, unless sendmail_path points to sendmail (or something that - * claims to be sendmail), don't pass params (not a perfect fix, - * but it will do). - * - * @param string $to To - * @param string $subject Subject - * @param string $body Message Body - * @param string $header Additional Header(s) - * @param string|null $params Params - * - * @return bool - */ - private function mailPassthru($to, $subject, $body, $header, $params) - { - //Check overloading of mail function to avoid double-encoding - if (ini_get('mbstring.func_overload') & 1) { - $subject = $this->secureHeader($subject); - } else { - $subject = $this->encodeHeader($this->secureHeader($subject)); - } - //Calling mail() with null params breaks - if (!$this->UseSendmailOptions or null === $params) { - $result = @mail($to, $subject, $body, $header); - } else { - $result = @mail($to, $subject, $body, $header, $params); - } - - return $result; - } - - /** - * Output debugging info via user-defined method. - * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). - * - * @see PHPMailer::$Debugoutput - * @see PHPMailer::$SMTPDebug - * - * @param string $str - */ - protected function edebug($str) - { - if ($this->SMTPDebug <= 0) { - return; - } - //Is this a PSR-3 logger? - if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { - $this->Debugoutput->debug($str); - - return; - } - //Avoid clash with built-in function names - if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) { - call_user_func($this->Debugoutput, $str, $this->SMTPDebug); - - return; - } - switch ($this->Debugoutput) { - case 'error_log': - //Don't output, just log - error_log($str); - break; - case 'html': - //Cleans up output a bit for a better looking, HTML-safe output - echo htmlentities( - preg_replace('/[\r\n]+/', '', $str), - ENT_QUOTES, - 'UTF-8' - ), "
\n"; - break; - case 'echo': - default: - //Normalize line breaks - $str = preg_replace('/\r\n|\r/ms', "\n", $str); - echo gmdate('Y-m-d H:i:s'), - "\t", - //Trim trailing space - trim( - //Indent for readability, except for trailing break - str_replace( - "\n", - "\n \t ", - trim($str) - ) - ), - "\n"; - } - } - - /** - * Sets message type to HTML or plain. - * - * @param bool $isHtml True for HTML mode - */ - public function isHTML($isHtml = true) - { - if ($isHtml) { - $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; - } else { - $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; - } - } - - /** - * Send messages using SMTP. - */ - public function isSMTP() - { - $this->Mailer = 'smtp'; - } - - /** - * Send messages using PHP's mail() function. - */ - public function isMail() - { - $this->Mailer = 'mail'; - } - - /** - * Send messages using $Sendmail. - */ - public function isSendmail() - { - $ini_sendmail_path = ini_get('sendmail_path'); - - if (false === stripos($ini_sendmail_path, 'sendmail')) { - $this->Sendmail = '/usr/sbin/sendmail'; - } else { - $this->Sendmail = $ini_sendmail_path; - } - $this->Mailer = 'sendmail'; - } - - /** - * Send messages using qmail. - */ - public function isQmail() - { - $ini_sendmail_path = ini_get('sendmail_path'); - - if (false === stripos($ini_sendmail_path, 'qmail')) { - $this->Sendmail = '/var/qmail/bin/qmail-inject'; - } else { - $this->Sendmail = $ini_sendmail_path; - } - $this->Mailer = 'qmail'; - } - - /** - * Add a "To" address. - * - * @param string $address The email address to send to - * @param string $name - * - * @return bool true on success, false if address already used or invalid in some way - */ - public function addAddress($address, $name = '') - { - return $this->addOrEnqueueAnAddress('to', $address, $name); - } - - /** - * Add a "CC" address. - * - * @param string $address The email address to send to - * @param string $name - * - * @return bool true on success, false if address already used or invalid in some way - */ - public function addCC($address, $name = '') - { - return $this->addOrEnqueueAnAddress('cc', $address, $name); - } - - /** - * Add a "BCC" address. - * - * @param string $address The email address to send to - * @param string $name - * - * @return bool true on success, false if address already used or invalid in some way - */ - public function addBCC($address, $name = '') - { - return $this->addOrEnqueueAnAddress('bcc', $address, $name); - } - - /** - * Add a "Reply-To" address. - * - * @param string $address The email address to reply to - * @param string $name - * - * @return bool true on success, false if address already used or invalid in some way - */ - public function addReplyTo($address, $name = '') - { - return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); - } - - /** - * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer - * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still - * be modified after calling this function), addition of such addresses is delayed until send(). - * Addresses that have been added already return false, but do not throw exceptions. - * - * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' - * @param string $address The email address to send, resp. to reply to - * @param string $name - * - * @throws Exception - * - * @return bool true on success, false if address already used or invalid in some way - */ - protected function addOrEnqueueAnAddress($kind, $address, $name) - { - $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim - $pos = strrpos($address, '@'); - if (false === $pos) { - // At-sign is missing. - $error_message = sprintf('%s (%s): %s', - $this->lang('invalid_address'), - $kind, - $address); - $this->setError($error_message); - $this->edebug($error_message); - if ($this->exceptions) { - throw new Exception($error_message); - } - - return false; - } - $params = [$kind, $address, $name]; - // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. - if ($this->has8bitChars(substr($address, ++$pos)) and static::idnSupported()) { - if ('Reply-To' != $kind) { - if (!array_key_exists($address, $this->RecipientsQueue)) { - $this->RecipientsQueue[$address] = $params; - - return true; - } - } else { - if (!array_key_exists($address, $this->ReplyToQueue)) { - $this->ReplyToQueue[$address] = $params; - - return true; - } - } - - return false; - } - - // Immediately add standard addresses without IDN. - return call_user_func_array([$this, 'addAnAddress'], $params); - } - - /** - * Add an address to one of the recipient arrays or to the ReplyTo array. - * Addresses that have been added already return false, but do not throw exceptions. - * - * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' - * @param string $address The email address to send, resp. to reply to - * @param string $name - * - * @throws Exception - * - * @return bool true on success, false if address already used or invalid in some way - */ - protected function addAnAddress($kind, $address, $name = '') - { - if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { - $error_message = sprintf('%s: %s', - $this->lang('Invalid recipient kind'), - $kind); - $this->setError($error_message); - $this->edebug($error_message); - if ($this->exceptions) { - throw new Exception($error_message); - } - - return false; - } - if (!static::validateAddress($address)) { - $error_message = sprintf('%s (%s): %s', - $this->lang('invalid_address'), - $kind, - $address); - $this->setError($error_message); - $this->edebug($error_message); - if ($this->exceptions) { - throw new Exception($error_message); - } - - return false; - } - if ('Reply-To' != $kind) { - if (!array_key_exists(strtolower($address), $this->all_recipients)) { - $this->{$kind}[] = [$address, $name]; - $this->all_recipients[strtolower($address)] = true; - - return true; - } - } else { - if (!array_key_exists(strtolower($address), $this->ReplyTo)) { - $this->ReplyTo[strtolower($address)] = [$address, $name]; - - return true; - } - } - - return false; - } - - /** - * Parse and validate a string containing one or more RFC822-style comma-separated email addresses - * of the form "display name
" into an array of name/address pairs. - * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. - * Note that quotes in the name part are removed. - * - * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation - * - * @param string $addrstr The address list string - * @param bool $useimap Whether to use the IMAP extension to parse the list - * - * @return array - */ - public static function parseAddresses($addrstr, $useimap = true) - { - $addresses = []; - if ($useimap and function_exists('imap_rfc822_parse_adrlist')) { - //Use this built-in parser if it's available - $list = imap_rfc822_parse_adrlist($addrstr, ''); - foreach ($list as $address) { - if ('.SYNTAX-ERROR.' != $address->host) { - if (static::validateAddress($address->mailbox . '@' . $address->host)) { - $addresses[] = [ - 'name' => (property_exists($address, 'personal') ? $address->personal : ''), - 'address' => $address->mailbox . '@' . $address->host, - ]; - } - } - } - } else { - //Use this simpler parser - $list = explode(',', $addrstr); - foreach ($list as $address) { - $address = trim($address); - //Is there a separate name part? - if (strpos($address, '<') === false) { - //No separate name, just use the whole thing - if (static::validateAddress($address)) { - $addresses[] = [ - 'name' => '', - 'address' => $address, - ]; - } - } else { - list($name, $email) = explode('<', $address); - $email = trim(str_replace('>', '', $email)); - if (static::validateAddress($email)) { - $addresses[] = [ - 'name' => trim(str_replace(['"', "'"], '', $name)), - 'address' => $email, - ]; - } - } - } - } - - return $addresses; - } - - /** - * Set the From and FromName properties. - * - * @param string $address - * @param string $name - * @param bool $auto Whether to also set the Sender address, defaults to true - * - * @throws Exception - * - * @return bool - */ - public function setFrom($address, $name = '', $auto = true) - { - $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim - // Don't validate now addresses with IDN. Will be done in send(). - $pos = strrpos($address, '@'); - if (false === $pos or - (!$this->has8bitChars(substr($address, ++$pos)) or !static::idnSupported()) and - !static::validateAddress($address)) { - $error_message = sprintf('%s (From): %s', - $this->lang('invalid_address'), - $address); - $this->setError($error_message); - $this->edebug($error_message); - if ($this->exceptions) { - throw new Exception($error_message); - } - - return false; - } - $this->From = $address; - $this->FromName = $name; - if ($auto) { - if (empty($this->Sender)) { - $this->Sender = $address; - } - } - - return true; - } - - /** - * Return the Message-ID header of the last email. - * Technically this is the value from the last time the headers were created, - * but it's also the message ID of the last sent message except in - * pathological cases. - * - * @return string - */ - public function getLastMessageID() - { - return $this->lastMessageID; - } - - /** - * Check that a string looks like an email address. - * Validation patterns supported: - * * `auto` Pick best pattern automatically; - * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; - * * `pcre` Use old PCRE implementation; - * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; - * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. - * * `noregex` Don't use a regex: super fast, really dumb. - * Alternatively you may pass in a callable to inject your own validator, for example: - * - * ```php - * PHPMailer::validateAddress('user@example.com', function($address) { - * return (strpos($address, '@') !== false); - * }); - * ``` - * - * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. - * - * @param string $address The email address to check - * @param string|callable $patternselect Which pattern to use - * - * @return bool - */ - public static function validateAddress($address, $patternselect = null) - { - if (null === $patternselect) { - $patternselect = static::$validator; - } - if (is_callable($patternselect)) { - return call_user_func($patternselect, $address); - } - //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 - if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) { - return false; - } - switch ($patternselect) { - case 'pcre': //Kept for BC - case 'pcre8': - /* - * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL - * is based. - * In addition to the addresses allowed by filter_var, also permits: - * * dotless domains: `a@b` - * * comments: `1234 @ local(blah) .machine .example` - * * quoted elements: `'"test blah"@example.org'` - * * numeric TLDs: `a@b.123` - * * unbracketed IPv4 literals: `a@192.168.0.1` - * * IPv6 literals: 'first.last@[IPv6:a1::]' - * Not all of these will necessarily work for sending! - * - * @see http://squiloople.com/2009/12/20/email-address-validation/ - * @copyright 2009-2010 Michael Rushton - * Feel free to use and redistribute this code. But please keep this copyright notice. - */ - return (bool) preg_match( - '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . - '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . - '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . - '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . - '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . - '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . - '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . - '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . - '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', - $address - ); - case 'html5': - /* - * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. - * - * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) - */ - return (bool) preg_match( - '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . - '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', - $address - ); - case 'php': - default: - return (bool) filter_var($address, FILTER_VALIDATE_EMAIL); - } - } - - /** - * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the - * `intl` and `mbstring` PHP extensions. - * - * @return bool `true` if required functions for IDN support are present - */ - public static function idnSupported() - { - return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding'); - } - - /** - * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. - * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. - * This function silently returns unmodified address if: - * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) - * - Conversion to punycode is impossible (e.g. required PHP functions are not available) - * or fails for any reason (e.g. domain contains characters not allowed in an IDN). - * - * @see PHPMailer::$CharSet - * - * @param string $address The email address to convert - * - * @return string The encoded address in ASCII form - */ - public function punyencodeAddress($address) - { - // Verify we have required functions, CharSet, and at-sign. - $pos = strrpos($address, '@'); - if (static::idnSupported() and - !empty($this->CharSet) and - false !== $pos - ) { - $domain = substr($address, ++$pos); - // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. - if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) { - $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); - //Ignore IDE complaints about this line - method signature changed in PHP 5.4 - $errorcode = 0; - $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46); - if (false !== $punycode) { - return substr($address, 0, $pos) . $punycode; - } - } - } - - return $address; - } - - /** - * Create a message and send it. - * Uses the sending method specified by $Mailer. - * - * @throws Exception - * - * @return bool false on error - See the ErrorInfo property for details of the error - */ - public function send() - { - try { - if (!$this->preSend()) { - return false; - } - - return $this->postSend(); - } catch (Exception $exc) { - $this->mailHeader = ''; - $this->setError($exc->getMessage()); - if ($this->exceptions) { - throw $exc; - } - - return false; - } - } - - /** - * Prepare a message for sending. - * - * @throws Exception - * - * @return bool - */ - public function preSend() - { - if ('smtp' == $this->Mailer or - ('mail' == $this->Mailer and stripos(PHP_OS, 'WIN') === 0) - ) { - //SMTP mandates RFC-compliant line endings - //and it's also used with mail() on Windows - static::setLE("\r\n"); - } else { - //Maintain backward compatibility with legacy Linux command line mailers - static::setLE(PHP_EOL); - } - //Check for buggy PHP versions that add a header with an incorrect line break - if (ini_get('mail.add_x_header') == 1 - and 'mail' == $this->Mailer - and stripos(PHP_OS, 'WIN') === 0 - and ((version_compare(PHP_VERSION, '7.0.0', '>=') - and version_compare(PHP_VERSION, '7.0.17', '<')) - or (version_compare(PHP_VERSION, '7.1.0', '>=') - and version_compare(PHP_VERSION, '7.1.3', '<'))) - ) { - trigger_error( - 'Your version of PHP is affected by a bug that may result in corrupted messages.' . - ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . - ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', - E_USER_WARNING - ); - } - - try { - $this->error_count = 0; // Reset errors - $this->mailHeader = ''; - - // Dequeue recipient and Reply-To addresses with IDN - foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { - $params[1] = $this->punyencodeAddress($params[1]); - call_user_func_array([$this, 'addAnAddress'], $params); - } - if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { - throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); - } - - // Validate From, Sender, and ConfirmReadingTo addresses - foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { - $this->$address_kind = trim($this->$address_kind); - if (empty($this->$address_kind)) { - continue; - } - $this->$address_kind = $this->punyencodeAddress($this->$address_kind); - if (!static::validateAddress($this->$address_kind)) { - $error_message = sprintf('%s (%s): %s', - $this->lang('invalid_address'), - $address_kind, - $this->$address_kind); - $this->setError($error_message); - $this->edebug($error_message); - if ($this->exceptions) { - throw new Exception($error_message); - } - - return false; - } - } - - // Set whether the message is multipart/alternative - if ($this->alternativeExists()) { - $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; - } - - $this->setMessageType(); - // Refuse to send an empty message unless we are specifically allowing it - if (!$this->AllowEmpty and empty($this->Body)) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); - } - - //Trim subject consistently - $this->Subject = trim($this->Subject); - // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) - $this->MIMEHeader = ''; - $this->MIMEBody = $this->createBody(); - // createBody may have added some headers, so retain them - $tempheaders = $this->MIMEHeader; - $this->MIMEHeader = $this->createHeader(); - $this->MIMEHeader .= $tempheaders; - - // To capture the complete message when using mail(), create - // an extra header list which createHeader() doesn't fold in - if ('mail' == $this->Mailer) { - if (count($this->to) > 0) { - $this->mailHeader .= $this->addrAppend('To', $this->to); - } else { - $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); - } - $this->mailHeader .= $this->headerLine( - 'Subject', - $this->encodeHeader($this->secureHeader($this->Subject)) - ); - } - - // Sign with DKIM if enabled - if (!empty($this->DKIM_domain) - and !empty($this->DKIM_selector) - and (!empty($this->DKIM_private_string) - or (!empty($this->DKIM_private) - and static::isPermittedPath($this->DKIM_private) - and file_exists($this->DKIM_private) - ) - ) - ) { - $header_dkim = $this->DKIM_Add( - $this->MIMEHeader . $this->mailHeader, - $this->encodeHeader($this->secureHeader($this->Subject)), - $this->MIMEBody - ); - $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE . - static::normalizeBreaks($header_dkim) . static::$LE; - } - - return true; - } catch (Exception $exc) { - $this->setError($exc->getMessage()); - if ($this->exceptions) { - throw $exc; - } - - return false; - } - } - - /** - * Actually send a message via the selected mechanism. - * - * @throws Exception - * - * @return bool - */ - public function postSend() - { - try { - // Choose the mailer and send through it - switch ($this->Mailer) { - case 'sendmail': - case 'qmail': - return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); - case 'smtp': - return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); - case 'mail': - return $this->mailSend($this->MIMEHeader, $this->MIMEBody); - default: - $sendMethod = $this->Mailer . 'Send'; - if (method_exists($this, $sendMethod)) { - return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); - } - - return $this->mailSend($this->MIMEHeader, $this->MIMEBody); - } - } catch (Exception $exc) { - $this->setError($exc->getMessage()); - $this->edebug($exc->getMessage()); - if ($this->exceptions) { - throw $exc; - } - } - - return false; - } - - /** - * Send mail using the $Sendmail program. - * - * @see PHPMailer::$Sendmail - * - * @param string $header The message headers - * @param string $body The message body - * - * @throws Exception - * - * @return bool - */ - protected function sendmailSend($header, $body) - { - // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. - if (!empty($this->Sender) and self::isShellSafe($this->Sender)) { - if ('qmail' == $this->Mailer) { - $sendmailFmt = '%s -f%s'; - } else { - $sendmailFmt = '%s -oi -f%s -t'; - } - } else { - if ('qmail' == $this->Mailer) { - $sendmailFmt = '%s'; - } else { - $sendmailFmt = '%s -oi -t'; - } - } - - $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); - - if ($this->SingleTo) { - foreach ($this->SingleToArray as $toAddr) { - $mail = @popen($sendmail, 'w'); - if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); - } - fwrite($mail, 'To: ' . $toAddr . "\n"); - fwrite($mail, $header); - fwrite($mail, $body); - $result = pclose($mail); - $this->doCallback( - ($result == 0), - [$toAddr], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); - if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); - } - } - } else { - $mail = @popen($sendmail, 'w'); - if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); - } - fwrite($mail, $header); - fwrite($mail, $body); - $result = pclose($mail); - $this->doCallback( - ($result == 0), - $this->to, - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); - if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); - } - } - - return true; - } - - /** - * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. - * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. - * - * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report - * - * @param string $string The string to be validated - * - * @return bool - */ - protected static function isShellSafe($string) - { - // Future-proof - if (escapeshellcmd($string) !== $string - or !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) - ) { - return false; - } - - $length = strlen($string); - - for ($i = 0; $i < $length; ++$i) { - $c = $string[$i]; - - // All other characters have a special meaning in at least one common shell, including = and +. - // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. - // Note that this does permit non-Latin alphanumeric characters based on the current locale. - if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { - return false; - } - } - - return true; - } - - /** - * Check whether a file path is of a permitted type. - * Used to reject URLs and phar files from functions that access local file paths, - * such as addAttachment. - * - * @param string $path A relative or absolute path to a file - * - * @return bool - */ - protected static function isPermittedPath($path) - { - return !preg_match('#^[a-z]+://#i', $path); - } - - /** - * Send mail using the PHP mail() function. - * - * @see http://www.php.net/manual/en/book.mail.php - * - * @param string $header The message headers - * @param string $body The message body - * - * @throws Exception - * - * @return bool - */ - protected function mailSend($header, $body) - { - $toArr = []; - foreach ($this->to as $toaddr) { - $toArr[] = $this->addrFormat($toaddr); - } - $to = implode(', ', $toArr); - - $params = null; - //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver - if (!empty($this->Sender) and static::validateAddress($this->Sender)) { - //A space after `-f` is optional, but there is a long history of its presence - //causing problems, so we don't use one - //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html - //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html - //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html - //Example problem: https://www.drupal.org/node/1057954 - // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. - if (self::isShellSafe($this->Sender)) { - $params = sprintf('-f%s', $this->Sender); - } - } - if (!empty($this->Sender) and static::validateAddress($this->Sender)) { - $old_from = ini_get('sendmail_from'); - ini_set('sendmail_from', $this->Sender); - } - $result = false; - if ($this->SingleTo and count($toArr) > 1) { - foreach ($toArr as $toAddr) { - $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); - $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); - } - } else { - $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); - $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); - } - if (isset($old_from)) { - ini_set('sendmail_from', $old_from); - } - if (!$result) { - throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); - } - - return true; - } - - /** - * Get an instance to use for SMTP operations. - * Override this function to load your own SMTP implementation, - * or set one with setSMTPInstance. - * - * @return SMTP - */ - public function getSMTPInstance() - { - if (!is_object($this->smtp)) { - $this->smtp = new SMTP(); - } - - return $this->smtp; - } - - /** - * Provide an instance to use for SMTP operations. - * - * @param SMTP $smtp - * - * @return SMTP - */ - public function setSMTPInstance(SMTP $smtp) - { - $this->smtp = $smtp; - - return $this->smtp; - } - - /** - * Send mail via SMTP. - * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. - * - * @see PHPMailer::setSMTPInstance() to use a different class. - * - * @uses \PHPMailer\PHPMailer\SMTP - * - * @param string $header The message headers - * @param string $body The message body - * - * @throws Exception - * - * @return bool - */ - protected function smtpSend($header, $body) - { - $bad_rcpt = []; - if (!$this->smtpConnect($this->SMTPOptions)) { - throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); - } - //Sender already validated in preSend() - if ('' == $this->Sender) { - $smtp_from = $this->From; - } else { - $smtp_from = $this->Sender; - } - if (!$this->smtp->mail($smtp_from)) { - $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); - throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); - } - - $callbacks = []; - // Attempt to send to all recipients - foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { - foreach ($togroup as $to) { - if (!$this->smtp->recipient($to[0])) { - $error = $this->smtp->getError(); - $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; - $isSent = false; - } else { - $isSent = true; - } - - $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]]; - } - } - - // Only send the DATA command if we have viable recipients - if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) { - throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); - } - - $smtp_transaction_id = $this->smtp->getLastTransactionID(); - - if ($this->SMTPKeepAlive) { - $this->smtp->reset(); - } else { - $this->smtp->quit(); - $this->smtp->close(); - } - - foreach ($callbacks as $cb) { - $this->doCallback( - $cb['issent'], - [$cb['to']], - [], - [], - $this->Subject, - $body, - $this->From, - ['smtp_transaction_id' => $smtp_transaction_id] - ); - } - - //Create error message for any bad addresses - if (count($bad_rcpt) > 0) { - $errstr = ''; - foreach ($bad_rcpt as $bad) { - $errstr .= $bad['to'] . ': ' . $bad['error']; - } - throw new Exception( - $this->lang('recipients_failed') . $errstr, - self::STOP_CONTINUE - ); - } - - return true; - } - - /** - * Initiate a connection to an SMTP server. - * Returns false if the operation failed. - * - * @param array $options An array of options compatible with stream_context_create() - * - * @throws Exception - * - * @uses \PHPMailer\PHPMailer\SMTP - * - * @return bool - */ - public function smtpConnect($options = null) - { - if (null === $this->smtp) { - $this->smtp = $this->getSMTPInstance(); - } - - //If no options are provided, use whatever is set in the instance - if (null === $options) { - $options = $this->SMTPOptions; - } - - // Already connected? - if ($this->smtp->connected()) { - return true; - } - - $this->smtp->setTimeout($this->Timeout); - $this->smtp->setDebugLevel($this->SMTPDebug); - $this->smtp->setDebugOutput($this->Debugoutput); - $this->smtp->setVerp($this->do_verp); - $hosts = explode(';', $this->Host); - $lastexception = null; - - foreach ($hosts as $hostentry) { - $hostinfo = []; - if (!preg_match( - '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*|\[[a-fA-F0-9:]+\]):?([0-9]*)$/', - trim($hostentry), - $hostinfo - )) { - static::edebug($this->lang('connect_host') . ' ' . $hostentry); - // Not a valid host entry - continue; - } - // $hostinfo[2]: optional ssl or tls prefix - // $hostinfo[3]: the hostname - // $hostinfo[4]: optional port number - // The host string prefix can temporarily override the current setting for SMTPSecure - // If it's not specified, the default value is used - - //Check the host name is a valid name or IP address before trying to use it - if (!static::isValidHost($hostinfo[3])) { - static::edebug($this->lang('connect_host') . ' ' . $hostentry); - continue; - } - $prefix = ''; - $secure = $this->SMTPSecure; - $tls = ('tls' == $this->SMTPSecure); - if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { - $prefix = 'ssl://'; - $tls = false; // Can't have SSL and TLS at the same time - $secure = 'ssl'; - } elseif ('tls' == $hostinfo[2]) { - $tls = true; - // tls doesn't use a prefix - $secure = 'tls'; - } - //Do we need the OpenSSL extension? - $sslext = defined('OPENSSL_ALGO_SHA256'); - if ('tls' === $secure or 'ssl' === $secure) { - //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled - if (!$sslext) { - throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); - } - } - $host = $hostinfo[3]; - $port = $this->Port; - $tport = (int) $hostinfo[4]; - if ($tport > 0 and $tport < 65536) { - $port = $tport; - } - if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { - try { - if ($this->Helo) { - $hello = $this->Helo; - } else { - $hello = $this->serverHostname(); - } - $this->smtp->hello($hello); - //Automatically enable TLS encryption if: - // * it's not disabled - // * we have openssl extension - // * we are not already using SSL - // * the server offers STARTTLS - if ($this->SMTPAutoTLS and $sslext and 'ssl' != $secure and $this->smtp->getServerExt('STARTTLS')) { - $tls = true; - } - if ($tls) { - if (!$this->smtp->startTLS()) { - throw new Exception($this->lang('connect_host')); - } - // We must resend EHLO after TLS negotiation - $this->smtp->hello($hello); - } - if ($this->SMTPAuth) { - if (!$this->smtp->authenticate( - $this->Username, - $this->Password, - $this->AuthType, - $this->oauth - ) - ) { - throw new Exception($this->lang('authenticate')); - } - } - - return true; - } catch (Exception $exc) { - $lastexception = $exc; - $this->edebug($exc->getMessage()); - // We must have connected, but then failed TLS or Auth, so close connection nicely - $this->smtp->quit(); - } - } - } - // If we get here, all connection attempts have failed, so close connection hard - $this->smtp->close(); - // As we've caught all exceptions, just report whatever the last one was - if ($this->exceptions and null !== $lastexception) { - throw $lastexception; - } - - return false; - } - - /** - * Close the active SMTP session if one exists. - */ - public function smtpClose() - { - if (null !== $this->smtp) { - if ($this->smtp->connected()) { - $this->smtp->quit(); - $this->smtp->close(); - } - } - } - - /** - * Set the language for error messages. - * Returns false if it cannot load the language file. - * The default language is English. - * - * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") - * @param string $lang_path Path to the language file directory, with trailing separator (slash) - * - * @return bool - */ - public function setLanguage($langcode = 'en', $lang_path = '') - { - // Backwards compatibility for renamed language codes - $renamed_langcodes = [ - 'br' => 'pt_br', - 'cz' => 'cs', - 'dk' => 'da', - 'no' => 'nb', - 'se' => 'sv', - 'rs' => 'sr', - 'tg' => 'tl', - ]; - - if (isset($renamed_langcodes[$langcode])) { - $langcode = $renamed_langcodes[$langcode]; - } - - // Define full set of translatable strings in English - $PHPMAILER_LANG = [ - 'authenticate' => 'SMTP Error: Could not authenticate.', - 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', - 'data_not_accepted' => 'SMTP Error: data not accepted.', - 'empty_message' => 'Message body empty', - 'encoding' => 'Unknown encoding: ', - 'execute' => 'Could not execute: ', - 'file_access' => 'Could not access file: ', - 'file_open' => 'File Error: Could not open file: ', - 'from_failed' => 'The following From address failed: ', - 'instantiate' => 'Could not instantiate mail function.', - 'invalid_address' => 'Invalid address: ', - 'mailer_not_supported' => ' mailer is not supported.', - 'provide_address' => 'You must provide at least one recipient email address.', - 'recipients_failed' => 'SMTP Error: The following recipients failed: ', - 'signing' => 'Signing Error: ', - 'smtp_connect_failed' => 'SMTP connect() failed.', - 'smtp_error' => 'SMTP server error: ', - 'variable_set' => 'Cannot set or reset variable: ', - 'extension_missing' => 'Extension missing: ', - ]; - if (empty($lang_path)) { - // Calculate an absolute path so it can work if CWD is not here - $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; - } - //Validate $langcode - if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { - $langcode = 'en'; - } - $foundlang = true; - $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; - // There is no English translation file - if ('en' != $langcode) { - // Make sure language file path is readable - if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) { - $foundlang = false; - } else { - // Overwrite language-specific strings. - // This way we'll never have missing translation keys. - $foundlang = include $lang_file; - } - } - $this->language = $PHPMAILER_LANG; - - return (bool) $foundlang; // Returns false if language not found - } - - /** - * Get the array of strings for the current language. - * - * @return array - */ - public function getTranslations() - { - return $this->language; - } - - /** - * Create recipient headers. - * - * @param string $type - * @param array $addr An array of recipients, - * where each recipient is a 2-element indexed array with element 0 containing an address - * and element 1 containing a name, like: - * [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']] - * - * @return string - */ - public function addrAppend($type, $addr) - { - $addresses = []; - foreach ($addr as $address) { - $addresses[] = $this->addrFormat($address); - } - - return $type . ': ' . implode(', ', $addresses) . static::$LE; - } - - /** - * Format an address for use in a message header. - * - * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like - * ['joe@example.com', 'Joe User'] - * - * @return string - */ - public function addrFormat($addr) - { - if (empty($addr[1])) { // No name provided - return $this->secureHeader($addr[0]); - } - - return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader( - $addr[0] - ) . '>'; - } - - /** - * Word-wrap message. - * For use with mailers that do not automatically perform wrapping - * and for quoted-printable encoded messages. - * Original written by philippe. - * - * @param string $message The message to wrap - * @param int $length The line length to wrap to - * @param bool $qp_mode Whether to run in Quoted-Printable mode - * - * @return string - */ - public function wrapText($message, $length, $qp_mode = false) - { - if ($qp_mode) { - $soft_break = sprintf(' =%s', static::$LE); - } else { - $soft_break = static::$LE; - } - // If utf-8 encoding is used, we will need to make sure we don't - // split multibyte characters when we wrap - $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); - $lelen = strlen(static::$LE); - $crlflen = strlen(static::$LE); - - $message = static::normalizeBreaks($message); - //Remove a trailing line break - if (substr($message, -$lelen) == static::$LE) { - $message = substr($message, 0, -$lelen); - } - - //Split message into lines - $lines = explode(static::$LE, $message); - //Message will be rebuilt in here - $message = ''; - foreach ($lines as $line) { - $words = explode(' ', $line); - $buf = ''; - $firstword = true; - foreach ($words as $word) { - if ($qp_mode and (strlen($word) > $length)) { - $space_left = $length - strlen($buf) - $crlflen; - if (!$firstword) { - if ($space_left > 20) { - $len = $space_left; - if ($is_utf8) { - $len = $this->utf8CharBoundary($word, $len); - } elseif ('=' == substr($word, $len - 1, 1)) { - --$len; - } elseif ('=' == substr($word, $len - 2, 1)) { - $len -= 2; - } - $part = substr($word, 0, $len); - $word = substr($word, $len); - $buf .= ' ' . $part; - $message .= $buf . sprintf('=%s', static::$LE); - } else { - $message .= $buf . $soft_break; - } - $buf = ''; - } - while (strlen($word) > 0) { - if ($length <= 0) { - break; - } - $len = $length; - if ($is_utf8) { - $len = $this->utf8CharBoundary($word, $len); - } elseif ('=' == substr($word, $len - 1, 1)) { - --$len; - } elseif ('=' == substr($word, $len - 2, 1)) { - $len -= 2; - } - $part = substr($word, 0, $len); - $word = substr($word, $len); - - if (strlen($word) > 0) { - $message .= $part . sprintf('=%s', static::$LE); - } else { - $buf = $part; - } - } - } else { - $buf_o = $buf; - if (!$firstword) { - $buf .= ' '; - } - $buf .= $word; - - if (strlen($buf) > $length and '' != $buf_o) { - $message .= $buf_o . $soft_break; - $buf = $word; - } - } - $firstword = false; - } - $message .= $buf . static::$LE; - } - - return $message; - } - - /** - * Find the last character boundary prior to $maxLength in a utf-8 - * quoted-printable encoded string. - * Original written by Colin Brown. - * - * @param string $encodedText utf-8 QP text - * @param int $maxLength Find the last character boundary prior to this length - * - * @return int - */ - public function utf8CharBoundary($encodedText, $maxLength) - { - $foundSplitPos = false; - $lookBack = 3; - while (!$foundSplitPos) { - $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); - $encodedCharPos = strpos($lastChunk, '='); - if (false !== $encodedCharPos) { - // Found start of encoded character byte within $lookBack block. - // Check the encoded byte value (the 2 chars after the '=') - $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); - $dec = hexdec($hex); - if ($dec < 128) { - // Single byte character. - // If the encoded char was found at pos 0, it will fit - // otherwise reduce maxLength to start of the encoded char - if ($encodedCharPos > 0) { - $maxLength -= $lookBack - $encodedCharPos; - } - $foundSplitPos = true; - } elseif ($dec >= 192) { - // First byte of a multi byte character - // Reduce maxLength to split at start of character - $maxLength -= $lookBack - $encodedCharPos; - $foundSplitPos = true; - } elseif ($dec < 192) { - // Middle byte of a multi byte character, look further back - $lookBack += 3; - } - } else { - // No encoded character found - $foundSplitPos = true; - } - } - - return $maxLength; - } - - /** - * Apply word wrapping to the message body. - * Wraps the message body to the number of chars set in the WordWrap property. - * You should only do this to plain-text bodies as wrapping HTML tags may break them. - * This is called automatically by createBody(), so you don't need to call it yourself. - */ - public function setWordWrap() - { - if ($this->WordWrap < 1) { - return; - } - - switch ($this->message_type) { - case 'alt': - case 'alt_inline': - case 'alt_attach': - case 'alt_inline_attach': - $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap); - break; - default: - $this->Body = $this->wrapText($this->Body, $this->WordWrap); - break; - } - } - - /** - * Assemble message headers. - * - * @return string The assembled headers - */ - public function createHeader() - { - $result = ''; - - $result .= $this->headerLine('Date', '' == $this->MessageDate ? self::rfcDate() : $this->MessageDate); - - // To be created automatically by mail() - if ($this->SingleTo) { - if ('mail' != $this->Mailer) { - foreach ($this->to as $toaddr) { - $this->SingleToArray[] = $this->addrFormat($toaddr); - } - } - } else { - if (count($this->to) > 0) { - if ('mail' != $this->Mailer) { - $result .= $this->addrAppend('To', $this->to); - } - } elseif (count($this->cc) == 0) { - $result .= $this->headerLine('To', 'undisclosed-recipients:;'); - } - } - - $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]); - - // sendmail and mail() extract Cc from the header before sending - if (count($this->cc) > 0) { - $result .= $this->addrAppend('Cc', $this->cc); - } - - // sendmail and mail() extract Bcc from the header before sending - if (( - 'sendmail' == $this->Mailer or 'qmail' == $this->Mailer or 'mail' == $this->Mailer - ) - and count($this->bcc) > 0 - ) { - $result .= $this->addrAppend('Bcc', $this->bcc); - } - - if (count($this->ReplyTo) > 0) { - $result .= $this->addrAppend('Reply-To', $this->ReplyTo); - } - - // mail() sets the subject itself - if ('mail' != $this->Mailer) { - $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); - } - - // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 - // https://tools.ietf.org/html/rfc5322#section-3.6.4 - if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) { - $this->lastMessageID = $this->MessageID; - } else { - $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); - } - $result .= $this->headerLine('Message-ID', $this->lastMessageID); - if (null !== $this->Priority) { - $result .= $this->headerLine('X-Priority', $this->Priority); - } - if ('' == $this->XMailer) { - $result .= $this->headerLine( - 'X-Mailer', - 'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)' - ); - } else { - $myXmailer = trim($this->XMailer); - if ($myXmailer) { - $result .= $this->headerLine('X-Mailer', $myXmailer); - } - } - - if ('' != $this->ConfirmReadingTo) { - $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); - } - - // Add custom headers - foreach ($this->CustomHeader as $header) { - $result .= $this->headerLine( - trim($header[0]), - $this->encodeHeader(trim($header[1])) - ); - } - if (!$this->sign_key_file) { - $result .= $this->headerLine('MIME-Version', '1.0'); - $result .= $this->getMailMIME(); - } - - return $result; - } - - /** - * Get the message MIME type headers. - * - * @return string - */ - public function getMailMIME() - { - $result = ''; - $ismultipart = true; - switch ($this->message_type) { - case 'inline': - $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); - $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); - break; - case 'attach': - case 'inline_attach': - case 'alt_attach': - case 'alt_inline_attach': - $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';'); - $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); - break; - case 'alt': - case 'alt_inline': - $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); - $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); - break; - default: - // Catches case 'plain': and case '': - $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); - $ismultipart = false; - break; - } - // RFC1341 part 5 says 7bit is assumed if not specified - if (static::ENCODING_7BIT != $this->Encoding) { - // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE - if ($ismultipart) { - if (static::ENCODING_8BIT == $this->Encoding) { - $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT); - } - // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible - } else { - $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); - } - } - - if ('mail' != $this->Mailer) { - $result .= static::$LE; - } - - return $result; - } - - /** - * Returns the whole MIME message. - * Includes complete headers and body. - * Only valid post preSend(). - * - * @see PHPMailer::preSend() - * - * @return string - */ - public function getSentMIMEMessage() - { - return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody; - } - - /** - * Create a unique ID to use for boundaries. - * - * @return string - */ - protected function generateId() - { - $len = 32; //32 bytes = 256 bits - if (function_exists('random_bytes')) { - $bytes = random_bytes($len); - } elseif (function_exists('openssl_random_pseudo_bytes')) { - $bytes = openssl_random_pseudo_bytes($len); - } else { - //Use a hash to force the length to the same as the other methods - $bytes = hash('sha256', uniqid((string) mt_rand(), true), true); - } - - //We don't care about messing up base64 format here, just want a random string - return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true))); - } - - /** - * Assemble the message body. - * Returns an empty string on failure. - * - * @throws Exception - * - * @return string The assembled message body - */ - public function createBody() - { - $body = ''; - //Create unique IDs and preset boundaries - $this->uniqueid = $this->generateId(); - $this->boundary[1] = 'b1_' . $this->uniqueid; - $this->boundary[2] = 'b2_' . $this->uniqueid; - $this->boundary[3] = 'b3_' . $this->uniqueid; - - if ($this->sign_key_file) { - $body .= $this->getMailMIME() . static::$LE; - } - - $this->setWordWrap(); - - $bodyEncoding = $this->Encoding; - $bodyCharSet = $this->CharSet; - //Can we do a 7-bit downgrade? - if (static::ENCODING_8BIT == $bodyEncoding and !$this->has8bitChars($this->Body)) { - $bodyEncoding = static::ENCODING_7BIT; - //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit - $bodyCharSet = 'us-ascii'; - } - //If lines are too long, and we're not already using an encoding that will shorten them, - //change to quoted-printable transfer encoding for the body part only - if (static::ENCODING_BASE64 != $this->Encoding and static::hasLineLongerThanMax($this->Body)) { - $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE; - } - - $altBodyEncoding = $this->Encoding; - $altBodyCharSet = $this->CharSet; - //Can we do a 7-bit downgrade? - if (static::ENCODING_8BIT == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) { - $altBodyEncoding = static::ENCODING_7BIT; - //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit - $altBodyCharSet = 'us-ascii'; - } - //If lines are too long, and we're not already using an encoding that will shorten them, - //change to quoted-printable transfer encoding for the alt body part only - if (static::ENCODING_BASE64 != $altBodyEncoding and static::hasLineLongerThanMax($this->AltBody)) { - $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; - } - //Use this as a preamble in all multipart message types - $mimepre = 'This is a multi-part message in MIME format.' . static::$LE; - switch ($this->message_type) { - case 'inline': - $body .= $mimepre; - $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - $body .= $this->attachAll('inline', $this->boundary[1]); - break; - case 'attach': - $body .= $mimepre; - $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - $body .= $this->attachAll('attachment', $this->boundary[1]); - break; - case 'inline_attach': - $body .= $mimepre; - $body .= $this->textLine('--' . $this->boundary[1]); - $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); - $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - $body .= $this->attachAll('inline', $this->boundary[2]); - $body .= static::$LE; - $body .= $this->attachAll('attachment', $this->boundary[1]); - break; - case 'alt': - $body .= $mimepre; - $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); - $body .= $this->encodeString($this->AltBody, $altBodyEncoding); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - if (!empty($this->Ical)) { - $body .= $this->getBoundary($this->boundary[1], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=REQUEST', ''); - $body .= $this->encodeString($this->Ical, $this->Encoding); - $body .= static::$LE; - } - $body .= $this->endBoundary($this->boundary[1]); - break; - case 'alt_inline': - $body .= $mimepre; - $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); - $body .= $this->encodeString($this->AltBody, $altBodyEncoding); - $body .= static::$LE; - $body .= $this->textLine('--' . $this->boundary[1]); - $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); - $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - $body .= $this->attachAll('inline', $this->boundary[2]); - $body .= static::$LE; - $body .= $this->endBoundary($this->boundary[1]); - break; - case 'alt_attach': - $body .= $mimepre; - $body .= $this->textLine('--' . $this->boundary[1]); - $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); - $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); - $body .= $this->encodeString($this->AltBody, $altBodyEncoding); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - if (!empty($this->Ical)) { - $body .= $this->getBoundary($this->boundary[2], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=REQUEST', ''); - $body .= $this->encodeString($this->Ical, $this->Encoding); - } - $body .= $this->endBoundary($this->boundary[2]); - $body .= static::$LE; - $body .= $this->attachAll('attachment', $this->boundary[1]); - break; - case 'alt_inline_attach': - $body .= $mimepre; - $body .= $this->textLine('--' . $this->boundary[1]); - $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); - $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); - $body .= $this->encodeString($this->AltBody, $altBodyEncoding); - $body .= static::$LE; - $body .= $this->textLine('--' . $this->boundary[2]); - $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); - $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"'); - $body .= static::$LE; - $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); - $body .= $this->encodeString($this->Body, $bodyEncoding); - $body .= static::$LE; - $body .= $this->attachAll('inline', $this->boundary[3]); - $body .= static::$LE; - $body .= $this->endBoundary($this->boundary[2]); - $body .= static::$LE; - $body .= $this->attachAll('attachment', $this->boundary[1]); - break; - default: - // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types - //Reset the `Encoding` property in case we changed it for line length reasons - $this->Encoding = $bodyEncoding; - $body .= $this->encodeString($this->Body, $this->Encoding); - break; - } - - if ($this->isError()) { - $body = ''; - if ($this->exceptions) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); - } - } elseif ($this->sign_key_file) { - try { - if (!defined('PKCS7_TEXT')) { - throw new Exception($this->lang('extension_missing') . 'openssl'); - } - // @TODO would be nice to use php://temp streams here - $file = tempnam(sys_get_temp_dir(), 'mail'); - if (false === file_put_contents($file, $body)) { - throw new Exception($this->lang('signing') . ' Could not write temp file'); - } - $signed = tempnam(sys_get_temp_dir(), 'signed'); - //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 - if (empty($this->sign_extracerts_file)) { - $sign = @openssl_pkcs7_sign( - $file, - $signed, - 'file://' . realpath($this->sign_cert_file), - ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], - [] - ); - } else { - $sign = @openssl_pkcs7_sign( - $file, - $signed, - 'file://' . realpath($this->sign_cert_file), - ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], - [], - PKCS7_DETACHED, - $this->sign_extracerts_file - ); - } - @unlink($file); - if ($sign) { - $body = file_get_contents($signed); - @unlink($signed); - //The message returned by openssl contains both headers and body, so need to split them up - $parts = explode("\n\n", $body, 2); - $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; - $body = $parts[1]; - } else { - @unlink($signed); - throw new Exception($this->lang('signing') . openssl_error_string()); - } - } catch (Exception $exc) { - $body = ''; - if ($this->exceptions) { - throw $exc; - } - } - } - - return $body; - } - - /** - * Return the start of a message boundary. - * - * @param string $boundary - * @param string $charSet - * @param string $contentType - * @param string $encoding - * - * @return string - */ - protected function getBoundary($boundary, $charSet, $contentType, $encoding) - { - $result = ''; - if ('' == $charSet) { - $charSet = $this->CharSet; - } - if ('' == $contentType) { - $contentType = $this->ContentType; - } - if ('' == $encoding) { - $encoding = $this->Encoding; - } - $result .= $this->textLine('--' . $boundary); - $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); - $result .= static::$LE; - // RFC1341 part 5 says 7bit is assumed if not specified - if (static::ENCODING_7BIT != $encoding) { - $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); - } - $result .= static::$LE; - - return $result; - } - - /** - * Return the end of a message boundary. - * - * @param string $boundary - * - * @return string - */ - protected function endBoundary($boundary) - { - return static::$LE . '--' . $boundary . '--' . static::$LE; - } - - /** - * Set the message type. - * PHPMailer only supports some preset message types, not arbitrary MIME structures. - */ - protected function setMessageType() - { - $type = []; - if ($this->alternativeExists()) { - $type[] = 'alt'; - } - if ($this->inlineImageExists()) { - $type[] = 'inline'; - } - if ($this->attachmentExists()) { - $type[] = 'attach'; - } - $this->message_type = implode('_', $type); - if ('' == $this->message_type) { - //The 'plain' message_type refers to the message having a single body element, not that it is plain-text - $this->message_type = 'plain'; - } - } - - /** - * Format a header line. - * - * @param string $name - * @param string|int $value - * - * @return string - */ - public function headerLine($name, $value) - { - return $name . ': ' . $value . static::$LE; - } - - /** - * Return a formatted mail line. - * - * @param string $value - * - * @return string - */ - public function textLine($value) - { - return $value . static::$LE; - } - - /** - * Add an attachment from a path on the filesystem. - * Never use a user-supplied path to a file! - * Returns false if the file could not be found or read. - * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client. - * If you need to do that, fetch the resource yourself and pass it in via a local file or string. - * - * @param string $path Path to the attachment - * @param string $name Overrides the attachment name - * @param string $encoding File encoding (see $Encoding) - * @param string $type File extension (MIME) type - * @param string $disposition Disposition to use - * - * @throws Exception - * - * @return bool - */ - public function addAttachment($path, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'attachment') - { - try { - if (!static::isPermittedPath($path) || !@is_file($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); - } - - // If a MIME type is not specified, try to work it out from the file name - if ('' == $type) { - $type = static::filenameToType($path); - } - - $filename = basename($path); - if ('' == $name) { - $name = $filename; - } - - $this->attachment[] = [ - 0 => $path, - 1 => $filename, - 2 => $name, - 3 => $encoding, - 4 => $type, - 5 => false, // isStringAttachment - 6 => $disposition, - 7 => $name, - ]; - } catch (Exception $exc) { - $this->setError($exc->getMessage()); - $this->edebug($exc->getMessage()); - if ($this->exceptions) { - throw $exc; - } - - return false; - } - - return true; - } - - /** - * Return the array of attachments. - * - * @return array - */ - public function getAttachments() - { - return $this->attachment; - } - - /** - * Attach all file, string, and binary attachments to the message. - * Returns an empty string on failure. - * - * @param string $disposition_type - * @param string $boundary - * - * @return string - */ - protected function attachAll($disposition_type, $boundary) - { - // Return text of body - $mime = []; - $cidUniq = []; - $incl = []; - - // Add all attachments - foreach ($this->attachment as $attachment) { - // Check if it is a valid disposition_filter - if ($attachment[6] == $disposition_type) { - // Check for string attachment - $string = ''; - $path = ''; - $bString = $attachment[5]; - if ($bString) { - $string = $attachment[0]; - } else { - $path = $attachment[0]; - } - - $inclhash = hash('sha256', serialize($attachment)); - if (in_array($inclhash, $incl)) { - continue; - } - $incl[] = $inclhash; - $name = $attachment[2]; - $encoding = $attachment[3]; - $type = $attachment[4]; - $disposition = $attachment[6]; - $cid = $attachment[7]; - if ('inline' == $disposition and array_key_exists($cid, $cidUniq)) { - continue; - } - $cidUniq[$cid] = true; - - $mime[] = sprintf('--%s%s', $boundary, static::$LE); - //Only include a filename property if we have one - if (!empty($name)) { - $mime[] = sprintf( - 'Content-Type: %s; name="%s"%s', - $type, - $this->encodeHeader($this->secureHeader($name)), - static::$LE - ); - } else { - $mime[] = sprintf( - 'Content-Type: %s%s', - $type, - static::$LE - ); - } - // RFC1341 part 5 says 7bit is assumed if not specified - if (static::ENCODING_7BIT != $encoding) { - $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE); - } - - if (!empty($cid)) { - $mime[] = sprintf('Content-ID: <%s>%s', $cid, static::$LE); - } - - // If a filename contains any of these chars, it should be quoted, - // but not otherwise: RFC2183 & RFC2045 5.1 - // Fixes a warning in IETF's msglint MIME checker - // Allow for bypassing the Content-Disposition header totally - if (!(empty($disposition))) { - $encoded_name = $this->encodeHeader($this->secureHeader($name)); - if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) { - $mime[] = sprintf( - 'Content-Disposition: %s; filename="%s"%s', - $disposition, - $encoded_name, - static::$LE . static::$LE - ); - } else { - if (!empty($encoded_name)) { - $mime[] = sprintf( - 'Content-Disposition: %s; filename=%s%s', - $disposition, - $encoded_name, - static::$LE . static::$LE - ); - } else { - $mime[] = sprintf( - 'Content-Disposition: %s%s', - $disposition, - static::$LE . static::$LE - ); - } - } - } else { - $mime[] = static::$LE; - } - - // Encode as string attachment - if ($bString) { - $mime[] = $this->encodeString($string, $encoding); - } else { - $mime[] = $this->encodeFile($path, $encoding); - } - if ($this->isError()) { - return ''; - } - $mime[] = static::$LE; - } - } - - $mime[] = sprintf('--%s--%s', $boundary, static::$LE); - - return implode('', $mime); - } - - /** - * Encode a file attachment in requested format. - * Returns an empty string on failure. - * - * @param string $path The full path to the file - * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' - * - * @throws Exception - * - * @return string - */ - protected function encodeFile($path, $encoding = self::ENCODING_BASE64) - { - try { - if (!static::isPermittedPath($path) || !file_exists($path)) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); - } - $file_buffer = file_get_contents($path); - if (false === $file_buffer) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); - } - $file_buffer = $this->encodeString($file_buffer, $encoding); - - return $file_buffer; - } catch (Exception $exc) { - $this->setError($exc->getMessage()); - - return ''; - } - } - - /** - * Encode a string in requested format. - * Returns an empty string on failure. - * - * @param string $str The text to encode - * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' - * - * @return string - */ - public function encodeString($str, $encoding = self::ENCODING_BASE64) - { - $encoded = ''; - switch (strtolower($encoding)) { - case static::ENCODING_BASE64: - $encoded = chunk_split( - base64_encode($str), - static::STD_LINE_LENGTH, - static::$LE - ); - break; - case static::ENCODING_7BIT: - case static::ENCODING_8BIT: - $encoded = static::normalizeBreaks($str); - // Make sure it ends with a line break - if (substr($encoded, -(strlen(static::$LE))) != static::$LE) { - $encoded .= static::$LE; - } - break; - case static::ENCODING_BINARY: - $encoded = $str; - break; - case static::ENCODING_QUOTED_PRINTABLE: - $encoded = $this->encodeQP($str); - break; - default: - $this->setError($this->lang('encoding') . $encoding); - break; - } - - return $encoded; - } - - /** - * Encode a header value (not including its label) optimally. - * Picks shortest of Q, B, or none. Result includes folding if needed. - * See RFC822 definitions for phrase, comment and text positions. - * - * @param string $str The header value to encode - * @param string $position What context the string will be used in - * - * @return string - */ - public function encodeHeader($str, $position = 'text') - { - $matchcount = 0; - switch (strtolower($position)) { - case 'phrase': - if (!preg_match('/[\200-\377]/', $str)) { - // Can't use addslashes as we don't know the value of magic_quotes_sybase - $encoded = addcslashes($str, "\0..\37\177\\\""); - if (($str == $encoded) and !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { - return $encoded; - } - - return "\"$encoded\""; - } - $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); - break; - /* @noinspection PhpMissingBreakStatementInspection */ - case 'comment': - $matchcount = preg_match_all('/[()"]/', $str, $matches); - //fallthrough - case 'text': - default: - $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); - break; - } - - //RFCs specify a maximum line length of 78 chars, however mail() will sometimes - //corrupt messages with headers longer than 65 chars. See #818 - $lengthsub = 'mail' == $this->Mailer ? 13 : 0; - $maxlen = static::STD_LINE_LENGTH - $lengthsub; - // Try to select the encoding which should produce the shortest output - if ($matchcount > strlen($str) / 3) { - // More than a third of the content will need encoding, so B encoding will be most efficient - $encoding = 'B'; - //This calculation is: - // max line length - // - shorten to avoid mail() corruption - // - Q/B encoding char overhead ("` =??[QB]??=`") - // - charset name length - $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet); - if ($this->hasMultiBytes($str)) { - // Use a custom function which correctly encodes and wraps long - // multibyte strings without breaking lines within a character - $encoded = $this->base64EncodeWrapMB($str, "\n"); - } else { - $encoded = base64_encode($str); - $maxlen -= $maxlen % 4; - $encoded = trim(chunk_split($encoded, $maxlen, "\n")); - } - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); - } elseif ($matchcount > 0) { - //1 or more chars need encoding, use Q-encode - $encoding = 'Q'; - //Recalc max line length for Q encoding - see comments on B encode - $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet); - $encoded = $this->encodeQ($str, $position); - $encoded = $this->wrapText($encoded, $maxlen, true); - $encoded = str_replace('=' . static::$LE, "\n", trim($encoded)); - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); - } elseif (strlen($str) > $maxlen) { - //No chars need encoding, but line is too long, so fold it - $encoded = trim($this->wrapText($str, $maxlen, false)); - if ($str == $encoded) { - //Wrapping nicely didn't work, wrap hard instead - $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE)); - } - $encoded = str_replace(static::$LE, "\n", trim($encoded)); - $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded); - } else { - //No reformatting needed - return $str; - } - - return trim(static::normalizeBreaks($encoded)); - } - - /** - * Check if a string contains multi-byte characters. - * - * @param string $str multi-byte text to wrap encode - * - * @return bool - */ - public function hasMultiBytes($str) - { - if (function_exists('mb_strlen')) { - return strlen($str) > mb_strlen($str, $this->CharSet); - } - - // Assume no multibytes (we can't handle without mbstring functions anyway) - return false; - } - - /** - * Does a string contain any 8-bit chars (in any charset)? - * - * @param string $text - * - * @return bool - */ - public function has8bitChars($text) - { - return (bool) preg_match('/[\x80-\xFF]/', $text); - } - - /** - * Encode and wrap long multibyte strings for mail headers - * without breaking lines within a character. - * Adapted from a function by paravoid. - * - * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 - * - * @param string $str multi-byte text to wrap encode - * @param string $linebreak string to use as linefeed/end-of-line - * - * @return string - */ - public function base64EncodeWrapMB($str, $linebreak = null) - { - $start = '=?' . $this->CharSet . '?B?'; - $end = '?='; - $encoded = ''; - if (null === $linebreak) { - $linebreak = static::$LE; - } - - $mb_length = mb_strlen($str, $this->CharSet); - // Each line must have length <= 75, including $start and $end - $length = 75 - strlen($start) - strlen($end); - // Average multi-byte ratio - $ratio = $mb_length / strlen($str); - // Base64 has a 4:3 ratio - $avgLength = floor($length * $ratio * .75); - - for ($i = 0; $i < $mb_length; $i += $offset) { - $lookBack = 0; - do { - $offset = $avgLength - $lookBack; - $chunk = mb_substr($str, $i, $offset, $this->CharSet); - $chunk = base64_encode($chunk); - ++$lookBack; - } while (strlen($chunk) > $length); - $encoded .= $chunk . $linebreak; - } - - // Chomp the last linefeed - return substr($encoded, 0, -strlen($linebreak)); - } - - /** - * Encode a string in quoted-printable format. - * According to RFC2045 section 6.7. - * - * @param string $string The text to encode - * - * @return string - */ - public function encodeQP($string) - { - return static::normalizeBreaks(quoted_printable_encode($string)); - } - - /** - * Encode a string using Q encoding. - * - * @see http://tools.ietf.org/html/rfc2047#section-4.2 - * - * @param string $str the text to encode - * @param string $position Where the text is going to be used, see the RFC for what that means - * - * @return string - */ - public function encodeQ($str, $position = 'text') - { - // There should not be any EOL in the string - $pattern = ''; - $encoded = str_replace(["\r", "\n"], '', $str); - switch (strtolower($position)) { - case 'phrase': - // RFC 2047 section 5.3 - $pattern = '^A-Za-z0-9!*+\/ -'; - break; - /* - * RFC 2047 section 5.2. - * Build $pattern without including delimiters and [] - */ - /* @noinspection PhpMissingBreakStatementInspection */ - case 'comment': - $pattern = '\(\)"'; - /* Intentional fall through */ - case 'text': - default: - // RFC 2047 section 5.1 - // Replace every high ascii, control, =, ? and _ characters - /** @noinspection SuspiciousAssignmentsInspection */ - $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; - break; - } - $matches = []; - if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { - // If the string contains an '=', make sure it's the first thing we replace - // so as to avoid double-encoding - $eqkey = array_search('=', $matches[0]); - if (false !== $eqkey) { - unset($matches[0][$eqkey]); - array_unshift($matches[0], '='); - } - foreach (array_unique($matches[0]) as $char) { - $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); - } - } - // Replace spaces with _ (more readable than =20) - // RFC 2047 section 4.2(2) - return str_replace(' ', '_', $encoded); - } - - /** - * Add a string or binary attachment (non-filesystem). - * This method can be used to attach ascii or binary data, - * such as a BLOB record from a database. - * - * @param string $string String attachment data - * @param string $filename Name of the attachment - * @param string $encoding File encoding (see $Encoding) - * @param string $type File extension (MIME) type - * @param string $disposition Disposition to use - */ - public function addStringAttachment( - $string, - $filename, - $encoding = self::ENCODING_BASE64, - $type = '', - $disposition = 'attachment' - ) { - // If a MIME type is not specified, try to work it out from the file name - if ('' == $type) { - $type = static::filenameToType($filename); - } - // Append to $attachment array - $this->attachment[] = [ - 0 => $string, - 1 => $filename, - 2 => basename($filename), - 3 => $encoding, - 4 => $type, - 5 => true, // isStringAttachment - 6 => $disposition, - 7 => 0, - ]; - } - - /** - * Add an embedded (inline) attachment from a file. - * This can include images, sounds, and just about any other document type. - * These differ from 'regular' attachments in that they are intended to be - * displayed inline with the message, not just attached for download. - * This is used in HTML messages that embed the images - * the HTML refers to using the $cid value. - * Never use a user-supplied path to a file! - * - * @param string $path Path to the attachment - * @param string $cid Content ID of the attachment; Use this to reference - * the content when using an embedded image in HTML - * @param string $name Overrides the attachment name - * @param string $encoding File encoding (see $Encoding) - * @param string $type File MIME type - * @param string $disposition Disposition to use - * - * @return bool True on successfully adding an attachment - */ - public function addEmbeddedImage($path, $cid, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'inline') - { - if (!static::isPermittedPath($path) || !@is_file($path)) { - $this->setError($this->lang('file_access') . $path); - - return false; - } - - // If a MIME type is not specified, try to work it out from the file name - if ('' == $type) { - $type = static::filenameToType($path); - } - - $filename = basename($path); - if ('' == $name) { - $name = $filename; - } - - // Append to $attachment array - $this->attachment[] = [ - 0 => $path, - 1 => $filename, - 2 => $name, - 3 => $encoding, - 4 => $type, - 5 => false, // isStringAttachment - 6 => $disposition, - 7 => $cid, - ]; - - return true; - } - - /** - * Add an embedded stringified attachment. - * This can include images, sounds, and just about any other document type. - * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type. - * - * @param string $string The attachment binary data - * @param string $cid Content ID of the attachment; Use this to reference - * the content when using an embedded image in HTML - * @param string $name A filename for the attachment. If this contains an extension, - * PHPMailer will attempt to set a MIME type for the attachment. - * For example 'file.jpg' would get an 'image/jpeg' MIME type. - * @param string $encoding File encoding (see $Encoding), defaults to 'base64' - * @param string $type MIME type - will be used in preference to any automatically derived type - * @param string $disposition Disposition to use - * - * @return bool True on successfully adding an attachment - */ - public function addStringEmbeddedImage( - $string, - $cid, - $name = '', - $encoding = self::ENCODING_BASE64, - $type = '', - $disposition = 'inline' - ) { - // If a MIME type is not specified, try to work it out from the name - if ('' == $type and !empty($name)) { - $type = static::filenameToType($name); - } - - // Append to $attachment array - $this->attachment[] = [ - 0 => $string, - 1 => $name, - 2 => $name, - 3 => $encoding, - 4 => $type, - 5 => true, // isStringAttachment - 6 => $disposition, - 7 => $cid, - ]; - - return true; - } - - /** - * Check if an embedded attachment is present with this cid. - * - * @param string $cid - * - * @return bool - */ - protected function cidExists($cid) - { - foreach ($this->attachment as $attachment) { - if ('inline' == $attachment[6] and $cid == $attachment[7]) { - return true; - } - } - - return false; - } - - /** - * Check if an inline attachment is present. - * - * @return bool - */ - public function inlineImageExists() - { - foreach ($this->attachment as $attachment) { - if ('inline' == $attachment[6]) { - return true; - } - } - - return false; - } - - /** - * Check if an attachment (non-inline) is present. - * - * @return bool - */ - public function attachmentExists() - { - foreach ($this->attachment as $attachment) { - if ('attachment' == $attachment[6]) { - return true; - } - } - - return false; - } - - /** - * Check if this message has an alternative body set. - * - * @return bool - */ - public function alternativeExists() - { - return !empty($this->AltBody); - } - - /** - * Clear queued addresses of given kind. - * - * @param string $kind 'to', 'cc', or 'bcc' - */ - public function clearQueuedAddresses($kind) - { - $this->RecipientsQueue = array_filter( - $this->RecipientsQueue, - function ($params) use ($kind) { - return $params[0] != $kind; - } - ); - } - - /** - * Clear all To recipients. - */ - public function clearAddresses() - { - foreach ($this->to as $to) { - unset($this->all_recipients[strtolower($to[0])]); - } - $this->to = []; - $this->clearQueuedAddresses('to'); - } - - /** - * Clear all CC recipients. - */ - public function clearCCs() - { - foreach ($this->cc as $cc) { - unset($this->all_recipients[strtolower($cc[0])]); - } - $this->cc = []; - $this->clearQueuedAddresses('cc'); - } - - /** - * Clear all BCC recipients. - */ - public function clearBCCs() - { - foreach ($this->bcc as $bcc) { - unset($this->all_recipients[strtolower($bcc[0])]); - } - $this->bcc = []; - $this->clearQueuedAddresses('bcc'); - } - - /** - * Clear all ReplyTo recipients. - */ - public function clearReplyTos() - { - $this->ReplyTo = []; - $this->ReplyToQueue = []; - } - - /** - * Clear all recipient types. - */ - public function clearAllRecipients() - { - $this->to = []; - $this->cc = []; - $this->bcc = []; - $this->all_recipients = []; - $this->RecipientsQueue = []; - } - - /** - * Clear all filesystem, string, and binary attachments. - */ - public function clearAttachments() - { - $this->attachment = []; - } - - /** - * Clear all custom headers. - */ - public function clearCustomHeaders() - { - $this->CustomHeader = []; - } - - /** - * Add an error message to the error container. - * - * @param string $msg - */ - protected function setError($msg) - { - ++$this->error_count; - if ('smtp' == $this->Mailer and null !== $this->smtp) { - $lasterror = $this->smtp->getError(); - if (!empty($lasterror['error'])) { - $msg .= $this->lang('smtp_error') . $lasterror['error']; - if (!empty($lasterror['detail'])) { - $msg .= ' Detail: ' . $lasterror['detail']; - } - if (!empty($lasterror['smtp_code'])) { - $msg .= ' SMTP code: ' . $lasterror['smtp_code']; - } - if (!empty($lasterror['smtp_code_ex'])) { - $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; - } - } - } - $this->ErrorInfo = $msg; - } - - /** - * Return an RFC 822 formatted date. - * - * @return string - */ - public static function rfcDate() - { - // Set the time zone to whatever the default is to avoid 500 errors - // Will default to UTC if it's not set properly in php.ini - date_default_timezone_set(@date_default_timezone_get()); - - return date('D, j M Y H:i:s O'); - } - - /** - * Get the server hostname. - * Returns 'localhost.localdomain' if unknown. - * - * @return string - */ - protected function serverHostname() - { - $result = ''; - if (!empty($this->Hostname)) { - $result = $this->Hostname; - } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER)) { - $result = $_SERVER['SERVER_NAME']; - } elseif (function_exists('gethostname') and gethostname() !== false) { - $result = gethostname(); - } elseif (php_uname('n') !== false) { - $result = php_uname('n'); - } - if (!static::isValidHost($result)) { - return 'localhost.localdomain'; - } - - return $result; - } - - /** - * Validate whether a string contains a valid value to use as a hostname or IP address. - * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`. - * - * @param string $host The host name or IP address to check - * - * @return bool - */ - public static function isValidHost($host) - { - //Simple syntax limits - if (empty($host) - or !is_string($host) - or strlen($host) > 256 - ) { - return false; - } - //Looks like a bracketed IPv6 address - if (trim($host, '[]') != $host) { - return (bool) filter_var(trim($host, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); - } - //If removing all the dots results in a numeric string, it must be an IPv4 address. - //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names - if (is_numeric(str_replace('.', '', $host))) { - //Is it a valid IPv4 address? - return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); - } - if (filter_var('http://' . $host, FILTER_VALIDATE_URL)) { - //Is it a syntactically valid hostname? - return true; - } - - return false; - } - - /** - * Get an error message in the current language. - * - * @param string $key - * - * @return string - */ - protected function lang($key) - { - if (count($this->language) < 1) { - $this->setLanguage('en'); // set the default language - } - - if (array_key_exists($key, $this->language)) { - if ('smtp_connect_failed' == $key) { - //Include a link to troubleshooting docs on SMTP connection failure - //this is by far the biggest cause of support questions - //but it's usually not PHPMailer's fault. - return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; - } - - return $this->language[$key]; - } - - //Return the key as a fallback - return $key; - } - - /** - * Check if an error occurred. - * - * @return bool True if an error did occur - */ - public function isError() - { - return $this->error_count > 0; - } - - /** - * Add a custom header. - * $name value can be overloaded to contain - * both header name and value (name:value). - * - * @param string $name Custom header name - * @param string|null $value Header value - */ - public function addCustomHeader($name, $value = null) - { - if (null === $value) { - // Value passed in as name:value - $this->CustomHeader[] = explode(':', $name, 2); - } else { - $this->CustomHeader[] = [$name, $value]; - } - } - - /** - * Returns all custom headers. - * - * @return array - */ - public function getCustomHeaders() - { - return $this->CustomHeader; - } - - /** - * Create a message body from an HTML string. - * Automatically inlines images and creates a plain-text version by converting the HTML, - * overwriting any existing values in Body and AltBody. - * Do not source $message content from user input! - * $basedir is prepended when handling relative URLs, e.g. and must not be empty - * will look for an image file in $basedir/images/a.png and convert it to inline. - * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email) - * Converts data-uri images into embedded attachments. - * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly. - * - * @param string $message HTML message string - * @param string $basedir Absolute path to a base directory to prepend to relative paths to images - * @param bool|callable $advanced Whether to use the internal HTML to text converter - * or your own custom converter @see PHPMailer::html2text() - * - * @return string $message The transformed message Body - */ - public function msgHTML($message, $basedir = '', $advanced = false) - { - preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); - if (array_key_exists(2, $images)) { - if (strlen($basedir) > 1 && '/' != substr($basedir, -1)) { - // Ensure $basedir has a trailing / - $basedir .= '/'; - } - foreach ($images[2] as $imgindex => $url) { - // Convert data URIs into embedded images - //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" - if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) { - if (count($match) == 4 and static::ENCODING_BASE64 == $match[2]) { - $data = base64_decode($match[3]); - } elseif ('' == $match[2]) { - $data = rawurldecode($match[3]); - } else { - //Not recognised so leave it alone - continue; - } - //Hash the decoded data, not the URL so that the same data-URI image used in multiple places - //will only be embedded once, even if it used a different encoding - $cid = hash('sha256', $data) . '@phpmailer.0'; // RFC2392 S 2 - - if (!$this->cidExists($cid)) { - $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, static::ENCODING_BASE64, $match[1]); - } - $message = str_replace( - $images[0][$imgindex], - $images[1][$imgindex] . '="cid:' . $cid . '"', - $message - ); - continue; - } - if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths) - !empty($basedir) - // Ignore URLs containing parent dir traversal (..) - and (strpos($url, '..') === false) - // Do not change urls that are already inline images - and 0 !== strpos($url, 'cid:') - // Do not change absolute URLs, including anonymous protocol - and !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) - ) { - $filename = basename($url); - $directory = dirname($url); - if ('.' == $directory) { - $directory = ''; - } - $cid = hash('sha256', $url) . '@phpmailer.0'; // RFC2392 S 2 - if (strlen($basedir) > 1 and '/' != substr($basedir, -1)) { - $basedir .= '/'; - } - if (strlen($directory) > 1 and '/' != substr($directory, -1)) { - $directory .= '/'; - } - if ($this->addEmbeddedImage( - $basedir . $directory . $filename, - $cid, - $filename, - static::ENCODING_BASE64, - static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION)) - ) - ) { - $message = preg_replace( - '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', - $images[1][$imgindex] . '="cid:' . $cid . '"', - $message - ); - } - } - } - } - $this->isHTML(true); - // Convert all message body line breaks to LE, makes quoted-printable encoding work much better - $this->Body = static::normalizeBreaks($message); - $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced)); - if (!$this->alternativeExists()) { - $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.' - . static::$LE; - } - - return $this->Body; - } - - /** - * Convert an HTML string into plain text. - * This is used by msgHTML(). - * Note - older versions of this function used a bundled advanced converter - * which was removed for license reasons in #232. - * Example usage: - * - * ```php - * // Use default conversion - * $plain = $mail->html2text($html); - * // Use your own custom converter - * $plain = $mail->html2text($html, function($html) { - * $converter = new MyHtml2text($html); - * return $converter->get_text(); - * }); - * ``` - * - * @param string $html The HTML text to convert - * @param bool|callable $advanced Any boolean value to use the internal converter, - * or provide your own callable for custom conversion - * - * @return string - */ - public function html2text($html, $advanced = false) - { - if (is_callable($advanced)) { - return call_user_func($advanced, $html); - } - - return html_entity_decode( - trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), - ENT_QUOTES, - $this->CharSet - ); - } - - /** - * Get the MIME type for a file extension. - * - * @param string $ext File extension - * - * @return string MIME type of file - */ - public static function _mime_types($ext = '') - { - $mimes = [ - 'xl' => 'application/excel', - 'js' => 'application/javascript', - 'hqx' => 'application/mac-binhex40', - 'cpt' => 'application/mac-compactpro', - 'bin' => 'application/macbinary', - 'doc' => 'application/msword', - 'word' => 'application/msword', - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', - 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - 'class' => 'application/octet-stream', - 'dll' => 'application/octet-stream', - 'dms' => 'application/octet-stream', - 'exe' => 'application/octet-stream', - 'lha' => 'application/octet-stream', - 'lzh' => 'application/octet-stream', - 'psd' => 'application/octet-stream', - 'sea' => 'application/octet-stream', - 'so' => 'application/octet-stream', - 'oda' => 'application/oda', - 'pdf' => 'application/pdf', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'mif' => 'application/vnd.mif', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', - 'wbxml' => 'application/vnd.wap.wbxml', - 'wmlc' => 'application/vnd.wap.wmlc', - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dxr' => 'application/x-director', - 'dvi' => 'application/x-dvi', - 'gtar' => 'application/x-gtar', - 'php3' => 'application/x-httpd-php', - 'php4' => 'application/x-httpd-php', - 'php' => 'application/x-httpd-php', - 'phtml' => 'application/x-httpd-php', - 'phps' => 'application/x-httpd-php-source', - 'swf' => 'application/x-shockwave-flash', - 'sit' => 'application/x-stuffit', - 'tar' => 'application/x-tar', - 'tgz' => 'application/x-tar', - 'xht' => 'application/xhtml+xml', - 'xhtml' => 'application/xhtml+xml', - 'zip' => 'application/zip', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'm4a' => 'audio/mp4', - 'mpga' => 'audio/mpeg', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'ram' => 'audio/x-pn-realaudio', - 'rm' => 'audio/x-pn-realaudio', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'ra' => 'audio/x-realaudio', - 'wav' => 'audio/x-wav', - 'mka' => 'audio/x-matroska', - 'bmp' => 'image/bmp', - 'gif' => 'image/gif', - 'jpeg' => 'image/jpeg', - 'jpe' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'png' => 'image/png', - 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'webp' => 'image/webp', - 'heif' => 'image/heif', - 'heifs' => 'image/heif-sequence', - 'heic' => 'image/heic', - 'heics' => 'image/heic-sequence', - 'eml' => 'message/rfc822', - 'css' => 'text/css', - 'html' => 'text/html', - 'htm' => 'text/html', - 'shtml' => 'text/html', - 'log' => 'text/plain', - 'text' => 'text/plain', - 'txt' => 'text/plain', - 'rtx' => 'text/richtext', - 'rtf' => 'text/rtf', - 'vcf' => 'text/vcard', - 'vcard' => 'text/vcard', - 'ics' => 'text/calendar', - 'xml' => 'text/xml', - 'xsl' => 'text/xml', - 'wmv' => 'video/x-ms-wmv', - 'mpeg' => 'video/mpeg', - 'mpe' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mp4' => 'video/mp4', - 'm4v' => 'video/mp4', - 'mov' => 'video/quicktime', - 'qt' => 'video/quicktime', - 'rv' => 'video/vnd.rn-realvideo', - 'avi' => 'video/x-msvideo', - 'movie' => 'video/x-sgi-movie', - 'webm' => 'video/webm', - 'mkv' => 'video/x-matroska', - ]; - $ext = strtolower($ext); - if (array_key_exists($ext, $mimes)) { - return $mimes[$ext]; - } - - return 'application/octet-stream'; - } - - /** - * Map a file name to a MIME type. - * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. - * - * @param string $filename A file name or full path, does not need to exist as a file - * - * @return string - */ - public static function filenameToType($filename) - { - // In case the path is a URL, strip any query string before getting extension - $qpos = strpos($filename, '?'); - if (false !== $qpos) { - $filename = substr($filename, 0, $qpos); - } - $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION); - - return static::_mime_types($ext); - } - - /** - * Multi-byte-safe pathinfo replacement. - * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe. - * - * @see http://www.php.net/manual/en/function.pathinfo.php#107461 - * - * @param string $path A filename or path, does not need to exist as a file - * @param int|string $options Either a PATHINFO_* constant, - * or a string name to return only the specified piece - * - * @return string|array - */ - public static function mb_pathinfo($path, $options = null) - { - $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '']; - $pathinfo = []; - if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) { - if (array_key_exists(1, $pathinfo)) { - $ret['dirname'] = $pathinfo[1]; - } - if (array_key_exists(2, $pathinfo)) { - $ret['basename'] = $pathinfo[2]; - } - if (array_key_exists(5, $pathinfo)) { - $ret['extension'] = $pathinfo[5]; - } - if (array_key_exists(3, $pathinfo)) { - $ret['filename'] = $pathinfo[3]; - } - } - switch ($options) { - case PATHINFO_DIRNAME: - case 'dirname': - return $ret['dirname']; - case PATHINFO_BASENAME: - case 'basename': - return $ret['basename']; - case PATHINFO_EXTENSION: - case 'extension': - return $ret['extension']; - case PATHINFO_FILENAME: - case 'filename': - return $ret['filename']; - default: - return $ret; - } - } - - /** - * Set or reset instance properties. - * You should avoid this function - it's more verbose, less efficient, more error-prone and - * harder to debug than setting properties directly. - * Usage Example: - * `$mail->set('SMTPSecure', 'tls');` - * is the same as: - * `$mail->SMTPSecure = 'tls';`. - * - * @param string $name The property name to set - * @param mixed $value The value to set the property to - * - * @return bool - */ - public function set($name, $value = '') - { - if (property_exists($this, $name)) { - $this->$name = $value; - - return true; - } - $this->setError($this->lang('variable_set') . $name); - - return false; - } - - /** - * Strip newlines to prevent header injection. - * - * @param string $str - * - * @return string - */ - public function secureHeader($str) - { - return trim(str_replace(["\r", "\n"], '', $str)); - } - - /** - * Normalize line breaks in a string. - * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format. - * Defaults to CRLF (for message bodies) and preserves consecutive breaks. - * - * @param string $text - * @param string $breaktype What kind of line break to use; defaults to static::$LE - * - * @return string - */ - public static function normalizeBreaks($text, $breaktype = null) - { - if (null === $breaktype) { - $breaktype = static::$LE; - } - // Normalise to \n - $text = str_replace(["\r\n", "\r"], "\n", $text); - // Now convert LE as needed - if ("\n" !== $breaktype) { - $text = str_replace("\n", $breaktype, $text); - } - - return $text; - } - - /** - * Return the current line break format string. - * - * @return string - */ - public static function getLE() - { - return static::$LE; - } - - /** - * Set the line break format string, e.g. "\r\n". - * - * @param string $le - */ - protected static function setLE($le) - { - static::$LE = $le; - } - - /** - * Set the public and private key files and password for S/MIME signing. - * - * @param string $cert_filename - * @param string $key_filename - * @param string $key_pass Password for private key - * @param string $extracerts_filename Optional path to chain certificate - */ - public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') - { - $this->sign_cert_file = $cert_filename; - $this->sign_key_file = $key_filename; - $this->sign_key_pass = $key_pass; - $this->sign_extracerts_file = $extracerts_filename; - } - - /** - * Quoted-Printable-encode a DKIM header. - * - * @param string $txt - * - * @return string - */ - public function DKIM_QP($txt) - { - $line = ''; - $len = strlen($txt); - for ($i = 0; $i < $len; ++$i) { - $ord = ord($txt[$i]); - if (((0x21 <= $ord) and ($ord <= 0x3A)) or $ord == 0x3C or ((0x3E <= $ord) and ($ord <= 0x7E))) { - $line .= $txt[$i]; - } else { - $line .= '=' . sprintf('%02X', $ord); - } - } - - return $line; - } - - /** - * Generate a DKIM signature. - * - * @param string $signHeader - * - * @throws Exception - * - * @return string The DKIM signature value - */ - public function DKIM_Sign($signHeader) - { - if (!defined('PKCS7_TEXT')) { - if ($this->exceptions) { - throw new Exception($this->lang('extension_missing') . 'openssl'); - } - - return ''; - } - $privKeyStr = !empty($this->DKIM_private_string) ? - $this->DKIM_private_string : - file_get_contents($this->DKIM_private); - if ('' != $this->DKIM_passphrase) { - $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); - } else { - $privKey = openssl_pkey_get_private($privKeyStr); - } - if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { - openssl_pkey_free($privKey); - - return base64_encode($signature); - } - openssl_pkey_free($privKey); - - return ''; - } - - /** - * Generate a DKIM canonicalization header. - * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2. - * Canonicalized headers should *always* use CRLF, regardless of mailer setting. - * - * @see https://tools.ietf.org/html/rfc6376#section-3.4.2 - * - * @param string $signHeader Header - * - * @return string - */ - public function DKIM_HeaderC($signHeader) - { - //Unfold all header continuation lines - //Also collapses folded whitespace. - //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]` - //@see https://tools.ietf.org/html/rfc5322#section-2.2 - //That means this may break if you do something daft like put vertical tabs in your headers. - $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader); - $lines = explode("\r\n", $signHeader); - foreach ($lines as $key => $line) { - //If the header is missing a :, skip it as it's invalid - //This is likely to happen because the explode() above will also split - //on the trailing LE, leaving an empty line - if (strpos($line, ':') === false) { - continue; - } - list($heading, $value) = explode(':', $line, 2); - //Lower-case header name - $heading = strtolower($heading); - //Collapse white space within the value - $value = preg_replace('/[ \t]{2,}/', ' ', $value); - //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value - //But then says to delete space before and after the colon. - //Net result is the same as trimming both ends of the value. - //by elimination, the same applies to the field name - $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t"); - } - - return implode("\r\n", $lines); - } - - /** - * Generate a DKIM canonicalization body. - * Uses the 'simple' algorithm from RFC6376 section 3.4.3. - * Canonicalized bodies should *always* use CRLF, regardless of mailer setting. - * - * @see https://tools.ietf.org/html/rfc6376#section-3.4.3 - * - * @param string $body Message Body - * - * @return string - */ - public function DKIM_BodyC($body) - { - if (empty($body)) { - return "\r\n"; - } - // Normalize line endings to CRLF - $body = static::normalizeBreaks($body, "\r\n"); - - //Reduce multiple trailing line breaks to a single one - return rtrim($body, "\r\n") . "\r\n"; - } - - /** - * Create the DKIM header and body in a new message header. - * - * @param string $headers_line Header lines - * @param string $subject Subject - * @param string $body Body - * - * @return string - */ - public function DKIM_Add($headers_line, $subject, $body) - { - $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms - $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body - $DKIMquery = 'dns/txt'; // Query method - $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) - $subject_header = "Subject: $subject"; - $headers = explode(static::$LE, $headers_line); - $from_header = ''; - $to_header = ''; - $date_header = ''; - $current = ''; - $copiedHeaderFields = ''; - $foundExtraHeaders = []; - $extraHeaderKeys = ''; - $extraHeaderValues = ''; - $extraCopyHeaderFields = ''; - foreach ($headers as $header) { - if (strpos($header, 'From:') === 0) { - $from_header = $header; - $current = 'from_header'; - } elseif (strpos($header, 'To:') === 0) { - $to_header = $header; - $current = 'to_header'; - } elseif (strpos($header, 'Date:') === 0) { - $date_header = $header; - $current = 'date_header'; - } elseif (!empty($this->DKIM_extraHeaders)) { - foreach ($this->DKIM_extraHeaders as $extraHeader) { - if (strpos($header, $extraHeader . ':') === 0) { - $headerValue = $header; - foreach ($this->CustomHeader as $customHeader) { - if ($customHeader[0] === $extraHeader) { - $headerValue = trim($customHeader[0]) . - ': ' . - $this->encodeHeader(trim($customHeader[1])); - break; - } - } - $foundExtraHeaders[$extraHeader] = $headerValue; - $current = ''; - break; - } - } - } else { - if (!empty($$current) and strpos($header, ' =?') === 0) { - $$current .= $header; - } else { - $current = ''; - } - } - } - foreach ($foundExtraHeaders as $key => $value) { - $extraHeaderKeys .= ':' . $key; - $extraHeaderValues .= $value . "\r\n"; - if ($this->DKIM_copyHeaderFields) { - $extraCopyHeaderFields .= "\t|" . str_replace('|', '=7C', $this->DKIM_QP($value)) . ";\r\n"; - } - } - if ($this->DKIM_copyHeaderFields) { - $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); - $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); - $date = str_replace('|', '=7C', $this->DKIM_QP($date_header)); - $subject = str_replace('|', '=7C', $this->DKIM_QP($subject_header)); - $copiedHeaderFields = "\tz=$from\r\n" . - "\t|$to\r\n" . - "\t|$date\r\n" . - "\t|$subject;\r\n" . - $extraCopyHeaderFields; - } - $body = $this->DKIM_BodyC($body); - $DKIMlen = strlen($body); // Length of body - $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body - if ('' == $this->DKIM_identity) { - $ident = ''; - } else { - $ident = ' i=' . $this->DKIM_identity . ';'; - } - $dkimhdrs = 'DKIM-Signature: v=1; a=' . - $DKIMsignatureType . '; q=' . - $DKIMquery . '; l=' . - $DKIMlen . '; s=' . - $this->DKIM_selector . - ";\r\n" . - "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . - "\th=From:To:Date:Subject" . $extraHeaderKeys . ";\r\n" . - "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . - $copiedHeaderFields . - "\tbh=" . $DKIMb64 . ";\r\n" . - "\tb="; - $toSign = $this->DKIM_HeaderC( - $from_header . "\r\n" . - $to_header . "\r\n" . - $date_header . "\r\n" . - $subject_header . "\r\n" . - $extraHeaderValues . - $dkimhdrs - ); - $signed = $this->DKIM_Sign($toSign); - - return static::normalizeBreaks($dkimhdrs . $signed) . static::$LE; - } - - /** - * Detect if a string contains a line longer than the maximum line length - * allowed by RFC 2822 section 2.1.1. - * - * @param string $str - * - * @return bool - */ - public static function hasLineLongerThanMax($str) - { - return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str); - } - - /** - * Allows for public read access to 'to' property. - * Before the send() call, queued addresses (i.e. with IDN) are not yet included. - * - * @return array - */ - public function getToAddresses() - { - return $this->to; - } - - /** - * Allows for public read access to 'cc' property. - * Before the send() call, queued addresses (i.e. with IDN) are not yet included. - * - * @return array - */ - public function getCcAddresses() - { - return $this->cc; - } - - /** - * Allows for public read access to 'bcc' property. - * Before the send() call, queued addresses (i.e. with IDN) are not yet included. - * - * @return array - */ - public function getBccAddresses() - { - return $this->bcc; - } - - /** - * Allows for public read access to 'ReplyTo' property. - * Before the send() call, queued addresses (i.e. with IDN) are not yet included. - * - * @return array - */ - public function getReplyToAddresses() - { - return $this->ReplyTo; - } - - /** - * Allows for public read access to 'all_recipients' property. - * Before the send() call, queued addresses (i.e. with IDN) are not yet included. - * - * @return array - */ - public function getAllRecipientAddresses() - { - return $this->all_recipients; - } - - /** - * Perform a callback. - * - * @param bool $isSent - * @param array $to - * @param array $cc - * @param array $bcc - * @param string $subject - * @param string $body - * @param string $from - * @param array $extra - */ - protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra) - { - if (!empty($this->action_function) and is_callable($this->action_function)) { - call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra); - } - } - - /** - * Get the OAuth instance. - * - * @return OAuth - */ - public function getOAuth() - { - return $this->oauth; - } - - /** - * Set an OAuth instance. - * - * @param OAuth $oauth - */ - public function setOAuth(OAuth $oauth) - { - $this->oauth = $oauth; - } -} diff --git a/lib/PHPMailer/PHPMailer/Exception.php b/lib/PHPMailer/PHPMailer/Exception.php new file mode 100644 index 000000000..9a05dec3c --- /dev/null +++ b/lib/PHPMailer/PHPMailer/Exception.php @@ -0,0 +1,39 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer exception handler. + * + * @author Marcus Bointon + */ +class Exception extends \Exception +{ + /** + * Prettify error message output. + * + * @return string + */ + public function errorMessage() + { + return '' . htmlspecialchars($this->getMessage()) . "
\n"; + } +} diff --git a/lib/PHPMailer/PHPMailer/PHPMailer.php b/lib/PHPMailer/PHPMailer/PHPMailer.php new file mode 100644 index 000000000..52104924d --- /dev/null +++ b/lib/PHPMailer/PHPMailer/PHPMailer.php @@ -0,0 +1,4502 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer - PHP email creation and transport class. + * + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + */ +class PHPMailer +{ + const CHARSET_ISO88591 = 'iso-8859-1'; + const CHARSET_UTF8 = 'utf-8'; + + const CONTENT_TYPE_PLAINTEXT = 'text/plain'; + const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; + const CONTENT_TYPE_TEXT_HTML = 'text/html'; + const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; + const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; + const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; + + const ENCODING_7BIT = '7bit'; + const ENCODING_8BIT = '8bit'; + const ENCODING_BASE64 = 'base64'; + const ENCODING_BINARY = 'binary'; + const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; + + /** + * Email priority. + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * + * @var int + */ + public $Priority; + + /** + * The character set of the message. + * + * @var string + */ + public $CharSet = self::CHARSET_ISO88591; + + /** + * The MIME Content-type of the message. + * + * @var string + */ + public $ContentType = self::CONTENT_TYPE_PLAINTEXT; + + /** + * The message encoding. + * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". + * + * @var string + */ + public $Encoding = self::ENCODING_8BIT; + + /** + * Holds the most recent mailer error message. + * + * @var string + */ + public $ErrorInfo = ''; + + /** + * The From email address for the message. + * + * @var string + */ + public $From = 'root@localhost'; + + /** + * The From name of the message. + * + * @var string + */ + public $FromName = 'Root User'; + + /** + * The envelope sender of the message. + * This will usually be turned into a Return-Path header by the receiver, + * and is the address that bounces will be sent to. + * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP. + * + * @var string + */ + public $Sender = ''; + + /** + * The Subject of the message. + * + * @var string + */ + public $Subject = ''; + + /** + * An HTML or plain text message body. + * If HTML then call isHTML(true). + * + * @var string + */ + public $Body = ''; + + /** + * The plain-text message body. + * This body can be read by mail clients that do not have HTML email + * capability such as mutt & Eudora. + * Clients that can read HTML will view the normal Body. + * + * @var string + */ + public $AltBody = ''; + + /** + * An iCal message part body. + * Only supported in simple alt or alt_inline message types + * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator. + * + * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ + * @see http://kigkonsult.se/iCalcreator/ + * + * @var string + */ + public $Ical = ''; + + /** + * The complete compiled MIME message body. + * + * @var string + */ + protected $MIMEBody = ''; + + /** + * The complete compiled MIME message headers. + * + * @var string + */ + protected $MIMEHeader = ''; + + /** + * Extra headers that createHeader() doesn't fold in. + * + * @var string + */ + protected $mailHeader = ''; + + /** + * Word-wrap the message body to this number of chars. + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * + * @see static::STD_LINE_LENGTH + * + * @var int + */ + public $WordWrap = 0; + + /** + * Which method to use to send mail. + * Options: "mail", "sendmail", or "smtp". + * + * @var string + */ + public $Mailer = 'mail'; + + /** + * The path to the sendmail program. + * + * @var string + */ + public $Sendmail = '/usr/sbin/sendmail'; + + /** + * Whether mail() uses a fully sendmail-compatible MTA. + * One which supports sendmail's "-oi -f" options. + * + * @var bool + */ + public $UseSendmailOptions = true; + + /** + * The email address that a reading confirmation should be sent to, also known as read receipt. + * + * @var string + */ + public $ConfirmReadingTo = ''; + + /** + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * + * @var string + */ + public $Hostname = ''; + + /** + * An ID to be used in the Message-ID header. + * If empty, a unique id will be generated. + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * + * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 + * + * @var string + */ + public $MessageID = ''; + + /** + * The message Date to be used in the Date header. + * If empty, the current date will be added. + * + * @var string + */ + public $MessageDate = ''; + + /** + * SMTP hosts. + * Either a single hostname or multiple semicolon-delimited hostnames. + * You can also specify a different port + * for each host by using this format: [hostname:port] + * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). + * Hosts will be tried in order. + * + * @var string + */ + public $Host = 'localhost'; + + /** + * The default SMTP server port. + * + * @var int + */ + public $Port = 25; + + /** + * The SMTP HELO of the message. + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * + * @see PHPMailer::$Hostname + * + * @var string + */ + public $Helo = ''; + + /** + * What kind of encryption to use on the SMTP connection. + * Options: '', 'ssl' or 'tls'. + * + * @var string + */ + public $SMTPSecure = ''; + + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * + * @var bool + */ + public $SMTPAutoTLS = true; + + /** + * Whether to use SMTP authentication. + * Uses the Username and Password properties. + * + * @see PHPMailer::$Username + * @see PHPMailer::$Password + * + * @var bool + */ + public $SMTPAuth = false; + + /** + * Options array passed to stream_context_create when connecting via SMTP. + * + * @var array + */ + public $SMTPOptions = []; + + /** + * SMTP username. + * + * @var string + */ + public $Username = ''; + + /** + * SMTP password. + * + * @var string + */ + public $Password = ''; + + /** + * SMTP auth type. + * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified. + * + * @var string + */ + public $AuthType = ''; + + /** + * An instance of the PHPMailer OAuth class. + * + * @var OAuth + */ + protected $oauth; + + /** + * The SMTP server timeout in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timeout = 300; + + /** + * SMTP class debug output mode. + * Debug output level. + * Options: + * * `0` No output + * * `1` Commands + * * `2` Data and commands + * * `3` As 2 plus connection status + * * `4` Low-level data output. + * + * @see SMTP::$do_debug + * + * @var int + */ + public $SMTPDebug = 0; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @see SMTP::$Debugoutput + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to keep SMTP connection open after each message. + * If this is set to true then to close the connection + * requires an explicit call to smtpClose(). + * + * @var bool + */ + public $SMTPKeepAlive = false; + + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * + * @var bool + */ + public $SingleTo = false; + + /** + * Storage for addresses when SingleTo is enabled. + * + * @var array + */ + protected $SingleToArray = []; + + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * + * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Postfix VERP info + * + * @var bool + */ + public $do_verp = false; + + /** + * Whether to allow sending messages with an empty body. + * + * @var bool + */ + public $AllowEmpty = false; + + /** + * DKIM selector. + * + * @var string + */ + public $DKIM_selector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * + * @var string + */ + public $DKIM_identity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * + * @var string + */ + public $DKIM_passphrase = ''; + + /** + * DKIM signing domain name. + * + * @example 'example.com' + * + * @var string + */ + public $DKIM_domain = ''; + + /** + * DKIM Copy header field values for diagnostic use. + * + * @var bool + */ + public $DKIM_copyHeaderFields = true; + + /** + * DKIM Extra signing headers. + * + * @example ['List-Unsubscribe', 'List-Help'] + * + * @var array + */ + public $DKIM_extraHeaders = []; + + /** + * DKIM private key file path. + * + * @var string + */ + public $DKIM_private = ''; + + /** + * DKIM private key string. + * + * If set, takes precedence over `$DKIM_private`. + * + * @var string + */ + public $DKIM_private_string = ''; + + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: http://www.php.net/is_callable + * + * Parameters: + * bool $result result of the send action + * array $to email addresses of the recipients + * array $cc cc email addresses + * array $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * string $extra extra information of possible use + * "smtp_transaction_id' => last smtp transaction id + * + * @var string + */ + public $action_function = ''; + + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace for none, or a string to use. + * + * @var string + */ + public $XMailer = ''; + + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. + * + * @see PHPMailer::validateAddress() + * + * @var string|callable + */ + public static $validator = 'php'; + + /** + * An instance of the SMTP sender class. + * + * @var SMTP + */ + protected $smtp; + + /** + * The array of 'to' names and addresses. + * + * @var array + */ + protected $to = []; + + /** + * The array of 'cc' names and addresses. + * + * @var array + */ + protected $cc = []; + + /** + * The array of 'bcc' names and addresses. + * + * @var array + */ + protected $bcc = []; + + /** + * The array of reply-to names and addresses. + * + * @var array + */ + protected $ReplyTo = []; + + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * + * @var array + */ + protected $all_recipients = []; + + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + * + * @var array + */ + protected $RecipientsQueue = []; + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$ReplyTo + * + * @var array + */ + protected $ReplyToQueue = []; + + /** + * The array of attachments. + * + * @var array + */ + protected $attachment = []; + + /** + * The array of custom headers. + * + * @var array + */ + protected $CustomHeader = []; + + /** + * The most recent Message-ID (including angular brackets). + * + * @var string + */ + protected $lastMessageID = ''; + + /** + * The message's MIME type. + * + * @var string + */ + protected $message_type = ''; + + /** + * The array of MIME boundary strings. + * + * @var array + */ + protected $boundary = []; + + /** + * The array of available languages. + * + * @var array + */ + protected $language = []; + + /** + * The number of errors encountered. + * + * @var int + */ + protected $error_count = 0; + + /** + * The S/MIME certificate file path. + * + * @var string + */ + protected $sign_cert_file = ''; + + /** + * The S/MIME key file path. + * + * @var string + */ + protected $sign_key_file = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * + * @var string + */ + protected $sign_extracerts_file = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * + * @var string + */ + protected $sign_key_pass = ''; + + /** + * Whether to throw exceptions for errors. + * + * @var bool + */ + protected $exceptions = false; + + /** + * Unique ID used for message ID and boundaries. + * + * @var string + */ + protected $uniqueid = ''; + + /** + * The PHPMailer Version number. + * + * @var string + */ + const VERSION = '6.0.7'; + + /** + * Error severity: message only, continue processing. + * + * @var int + */ + const STOP_MESSAGE = 0; + + /** + * Error severity: message, likely ok to continue processing. + * + * @var int + */ + const STOP_CONTINUE = 1; + + /** + * Error severity: message, plus full stop, critical error reached. + * + * @var int + */ + const STOP_CRITICAL = 2; + + /** + * SMTP RFC standard line ending. + * + * @var string + */ + protected static $LE = "\r\n"; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * The lower maximum line length allowed by RFC 2822 section 2.1.1. + * This length does NOT include the line break + * 76 means that lines will be 77 or 78 chars depending on whether + * the line break format is LF or CRLF; both are valid. + * + * @var int + */ + const STD_LINE_LENGTH = 76; + + /** + * Constructor. + * + * @param bool $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) + { + if (null !== $exceptions) { + $this->exceptions = (bool) $exceptions; + } + //Pick an appropriate debug output format automatically + $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); + } + + /** + * Destructor. + */ + public function __destruct() + { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do). + * + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string|null $params Params + * + * @return bool + */ + private function mailPassthru($to, $subject, $body, $header, $params) + { + //Check overloading of mail function to avoid double-encoding + if (ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } + //Calling mail() with null params breaks + if (!$this->UseSendmailOptions or null === $params) { + $result = @mail($to, $subject, $body, $header); + } else { + $result = @mail($to, $subject, $body, $header, $params); + } + + return $result; + } + + /** + * Output debugging info via user-defined method. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). + * + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * + * @param string $str + */ + protected function edebug($str) + { + if ($this->SMTPDebug <= 0) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Sets message type to HTML or plain. + * + * @param bool $isHtml True for HTML mode + */ + public function isHTML($isHtml = true) + { + if ($isHtml) { + $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; + } else { + $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; + } + } + + /** + * Send messages using SMTP. + */ + public function isSMTP() + { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + */ + public function isMail() + { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + */ + public function isSendmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + */ + public function isQmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addAddress($address, $name = '') + { + return $this->addOrEnqueueAnAddress('to', $address, $name); + } + + /** + * Add a "CC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('cc', $address, $name); + } + + /** + * Add a "BCC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addBCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('bcc', $address, $name); + } + + /** + * Add a "Reply-To" address. + * + * @param string $address The email address to reply to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addReplyTo($address, $name = '') + { + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $pos = strrpos($address, '@'); + if (false === $pos) { + // At-sign is missing. + $error_message = sprintf('%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $params = [$kind, $address, $name]; + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if ($this->has8bitChars(substr($address, ++$pos)) and static::idnSupported()) { + if ('Reply-To' != $kind) { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + + return true; + } + } else { + if (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + + return true; + } + } + + return false; + } + + // Immediately add standard addresses without IDN. + return call_user_func_array([$this, 'addAnAddress'], $params); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addAnAddress($kind, $address, $name = '') + { + if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { + $error_message = sprintf('%s: %s', + $this->lang('Invalid recipient kind'), + $kind); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if (!static::validateAddress($address)) { + $error_message = sprintf('%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ('Reply-To' != $kind) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { + $this->{$kind}[] = [$address, $name]; + $this->all_recipients[strtolower($address)] = true; + + return true; + } + } else { + if (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = [$address, $name]; + + return true; + } + } + + return false; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * + * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + * + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * + * @return array + */ + public static function parseAddresses($addrstr, $useimap = true) + { + $addresses = []; + if ($useimap and function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if ('.SYNTAX-ERROR.' != $address->host) { + if (static::validateAddress($address->mailbox . '@' . $address->host)) { + $addresses[] = [ + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host, + ]; + } + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + if (static::validateAddress($email)) { + $addresses[] = [ + 'name' => trim(str_replace(['"', "'"], '', $name)), + 'address' => $email, + ]; + } + } + } + } + + return $addresses; + } + + /** + * Set the From and FromName properties. + * + * @param string $address + * @param string $name + * @param bool $auto Whether to also set the Sender address, defaults to true + * + * @throws Exception + * + * @return bool + */ + public function setFrom($address, $name = '', $auto = true) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + // Don't validate now addresses with IDN. Will be done in send(). + $pos = strrpos($address, '@'); + if (false === $pos or + (!$this->has8bitChars(substr($address, ++$pos)) or !static::idnSupported()) and + !static::validateAddress($address)) { + $error_message = sprintf('%s (From): %s', + $this->lang('invalid_address'), + $address); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $this->From = $address; + $this->FromName = $name; + if ($auto) { + if (empty($this->Sender)) { + $this->Sender = $address; + } + } + + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * + * @return string + */ + public function getLastMessageID() + { + return $this->lastMessageID; + } + + /** + * Check that a string looks like an email address. + * Validation patterns supported: + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * + * ```php + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * ``` + * + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * + * @param string $address The email address to check + * @param string|callable $patternselect Which pattern to use + * + * @return bool + */ + public static function validateAddress($address, $patternselect = null) + { + if (null === $patternselect) { + $patternselect = static::$validator; + } + if (is_callable($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) { + return false; + } + switch ($patternselect) { + case 'pcre': //Kept for BC + case 'pcre8': + /* + * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL + * is based. + * In addition to the addresses allowed by filter_var, also permits: + * * dotless domains: `a@b` + * * comments: `1234 @ local(blah) .machine .example` + * * quoted elements: `'"test blah"@example.org'` + * * numeric TLDs: `a@b.123` + * * unbracketed IPv4 literals: `a@192.168.0.1` + * * IPv6 literals: 'first.last@[IPv6:a1::]' + * Not all of these will necessarily work for sending! + * + * @see http://squiloople.com/2009/12/20/email-address-validation/ + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (bool) preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'html5': + /* + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * + * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (bool) preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'php': + default: + return (bool) filter_var($address, FILTER_VALIDATE_EMAIL); + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * `intl` and `mbstring` PHP extensions. + * + * @return bool `true` if required functions for IDN support are present + */ + public static function idnSupported() + { + return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain contains characters not allowed in an IDN). + * + * @see PHPMailer::$CharSet + * + * @param string $address The email address to convert + * + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + // Verify we have required functions, CharSet, and at-sign. + $pos = strrpos($address, '@'); + if (static::idnSupported() and + !empty($this->CharSet) and + false !== $pos + ) { + $domain = substr($address, ++$pos); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) { + $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); + //Ignore IDE complaints about this line - method signature changed in PHP 5.4 + $errorcode = 0; + $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46); + if (false !== $punycode) { + return substr($address, 0, $pos) . $punycode; + } + } + } + + return $address; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * + * @throws Exception + * + * @return bool false on error - See the ErrorInfo property for details of the error + */ + public function send() + { + try { + if (!$this->preSend()) { + return false; + } + + return $this->postSend(); + } catch (Exception $exc) { + $this->mailHeader = ''; + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Prepare a message for sending. + * + * @throws Exception + * + * @return bool + */ + public function preSend() + { + if ('smtp' == $this->Mailer or + ('mail' == $this->Mailer and stripos(PHP_OS, 'WIN') === 0) + ) { + //SMTP mandates RFC-compliant line endings + //and it's also used with mail() on Windows + static::setLE("\r\n"); + } else { + //Maintain backward compatibility with legacy Linux command line mailers + static::setLE(PHP_EOL); + } + //Check for buggy PHP versions that add a header with an incorrect line break + if (ini_get('mail.add_x_header') == 1 + and 'mail' == $this->Mailer + and stripos(PHP_OS, 'WIN') === 0 + and ((version_compare(PHP_VERSION, '7.0.0', '>=') + and version_compare(PHP_VERSION, '7.0.17', '<')) + or (version_compare(PHP_VERSION, '7.1.0', '>=') + and version_compare(PHP_VERSION, '7.1.3', '<'))) + ) { + trigger_error( + 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', + E_USER_WARNING + ); + } + + try { + $this->error_count = 0; // Reset errors + $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array([$this, 'addAnAddress'], $params); + } + if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { + throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + } + + // Validate From, Sender, and ConfirmReadingTo addresses + foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { + $this->$address_kind = trim($this->$address_kind); + if (empty($this->$address_kind)) { + continue; + } + $this->$address_kind = $this->punyencodeAddress($this->$address_kind); + if (!static::validateAddress($this->$address_kind)) { + $error_message = sprintf('%s (%s): %s', + $this->lang('invalid_address'), + $address_kind, + $this->$address_kind); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + } + + // Set whether the message is multipart/alternative + if ($this->alternativeExists()) { + $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; + } + + $this->setMessageType(); + // Refuse to send an empty message unless we are specifically allowing it + if (!$this->AllowEmpty and empty($this->Body)) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + + //Trim subject consistently + $this->Subject = trim($this->Subject); + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + // To capture the complete message when using mail(), create + // an extra header list which createHeader() doesn't fold in + if ('mail' == $this->Mailer) { + if (count($this->to) > 0) { + $this->mailHeader .= $this->addrAppend('To', $this->to); + } else { + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader($this->secureHeader($this->Subject)) + ); + } + + // Sign with DKIM if enabled + if (!empty($this->DKIM_domain) + and !empty($this->DKIM_selector) + and (!empty($this->DKIM_private_string) + or (!empty($this->DKIM_private) + and static::isPermittedPath($this->DKIM_private) + and file_exists($this->DKIM_private) + ) + ) + ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader($this->secureHeader($this->Subject)), + $this->MIMEBody + ); + $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE . + static::normalizeBreaks($header_dkim) . static::$LE; + } + + return true; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Actually send a message via the selected mechanism. + * + * @throws Exception + * + * @return bool + */ + public function postSend() + { + try { + // Choose the mailer and send through it + switch ($this->Mailer) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); + case 'smtp': + return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + default: + $sendMethod = $this->Mailer . 'Send'; + if (method_exists($this, $sendMethod)) { + return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); + } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + } + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + } + + return false; + } + + /** + * Send mail using the $Sendmail program. + * + * @see PHPMailer::$Sendmail + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function sendmailSend($header, $body) + { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) and self::isShellSafe($this->Sender)) { + if ('qmail' == $this->Mailer) { + $sendmailFmt = '%s -f%s'; + } else { + $sendmailFmt = '%s -oi -f%s -t'; + } + } else { + if ('qmail' == $this->Mailer) { + $sendmailFmt = '%s'; + } else { + $sendmailFmt = '%s -oi -t'; + } + } + + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, 'To: ' . $toAddr . "\n"); + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result == 0), + [$toAddr], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + } else { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result == 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + + return true; + } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * + * @param string $string The string to be validated + * + * @return bool + */ + protected static function isShellSafe($string) + { + // Future-proof + if (escapeshellcmd($string) !== $string + or !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; ++$i) { + $c = $string[$i]; + + // All other characters have a special meaning in at least one common shell, including = and +. + // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + // Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + + /** + * Check whether a file path is of a permitted type. + * Used to reject URLs and phar files from functions that access local file paths, + * such as addAttachment. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function isPermittedPath($path) + { + return !preg_match('#^[a-z]+://#i', $path); + } + + /** + * Send mail using the PHP mail() function. + * + * @see http://www.php.net/manual/en/book.mail.php + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function mailSend($header, $body) + { + $toArr = []; + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); + } + $to = implode(', ', $toArr); + + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + if (!empty($this->Sender) and static::validateAddress($this->Sender)) { + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html + //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html + //Example problem: https://www.drupal.org/node/1057954 + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } + } + if (!empty($this->Sender) and static::validateAddress($this->Sender)) { + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + } + $result = false; + if ($this->SingleTo and count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + } else { + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + if (!$result) { + throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + } + + return true; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation, + * or set one with setSMTPInstance. + * + * @return SMTP + */ + public function getSMTPInstance() + { + if (!is_object($this->smtp)) { + $this->smtp = new SMTP(); + } + + return $this->smtp; + } + + /** + * Provide an instance to use for SMTP operations. + * + * @param SMTP $smtp + * + * @return SMTP + */ + public function setSMTPInstance(SMTP $smtp) + { + $this->smtp = $smtp; + + return $this->smtp; + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * + * @see PHPMailer::setSMTPInstance() to use a different class. + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function smtpSend($header, $body) + { + $bad_rcpt = []; + if (!$this->smtpConnect($this->SMTPOptions)) { + throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + } + //Sender already validated in preSend() + if ('' == $this->Sender) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } + if (!$this->smtp->mail($smtp_from)) { + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); + } + + $callbacks = []; + // Attempt to send to all recipients + foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0])) { + $error = $this->smtp->getError(); + $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; + $isSent = false; + } else { + $isSent = true; + } + + $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]]; + } + } + + // Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) { + throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + } + + $smtp_transaction_id = $this->smtp->getLastTransactionID(); + + if ($this->SMTPKeepAlive) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + + foreach ($callbacks as $cb) { + $this->doCallback( + $cb['issent'], + [$cb['to']], + [], + [], + $this->Subject, + $body, + $this->From, + ['smtp_transaction_id' => $smtp_transaction_id] + ); + } + + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new Exception( + $this->lang('recipients_failed') . $errstr, + self::STOP_CONTINUE + ); + } + + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * + * @param array $options An array of options compatible with stream_context_create() + * + * @throws Exception + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @return bool + */ + public function smtpConnect($options = null) + { + if (null === $this->smtp) { + $this->smtp = $this->getSMTPInstance(); + } + + //If no options are provided, use whatever is set in the instance + if (null === $options) { + $options = $this->SMTPOptions; + } + + // Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = []; + if (!preg_match( + '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*|\[[a-fA-F0-9:]+\]):?([0-9]*)$/', + trim($hostentry), + $hostinfo + )) { + static::edebug($this->lang('connect_host') . ' ' . $hostentry); + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + + //Check the host name is a valid name or IP address before trying to use it + if (!static::isValidHost($hostinfo[3])) { + static::edebug($this->lang('connect_host') . ' ' . $hostentry); + continue; + } + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = ('tls' == $this->SMTPSecure); + if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; + } elseif ('tls' == $hostinfo[2]) { + $tls = true; + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA256'); + if ('tls' === $secure or 'ssl' === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[3]; + $port = $this->Port; + $tport = (int) $hostinfo[4]; + if ($tport > 0 and $tport < 65536) { + $port = $tport; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS and $sslext and 'ssl' != $secure and $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + throw new Exception($this->lang('connect_host')); + } + // We must resend EHLO after TLS negotiation + $this->smtp->hello($hello); + } + if ($this->SMTPAuth) { + if (!$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->oauth + ) + ) { + throw new Exception($this->lang('authenticate')); + } + } + + return true; + } catch (Exception $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + // If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + // As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions and null !== $lastexception) { + throw $lastexception; + } + + return false; + } + + /** + * Close the active SMTP session if one exists. + */ + public function smtpClose() + { + if (null !== $this->smtp) { + if ($this->smtp->connected()) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + } + + /** + * Set the language for error messages. + * Returns false if it cannot load the language file. + * The default language is English. + * + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * + * @return bool + */ + public function setLanguage($langcode = 'en', $lang_path = '') + { + // Backwards compatibility for renamed language codes + $renamed_langcodes = [ + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + 'rs' => 'sr', + 'tg' => 'tl', + ]; + + if (isset($renamed_langcodes[$langcode])) { + $langcode = $renamed_langcodes[$langcode]; + } + + // Define full set of translatable strings in English + $PHPMAILER_LANG = [ + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ', + ]; + if (empty($lang_path)) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; + } + //Validate $langcode + if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $langcode = 'en'; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ('en' != $langcode) { + // Make sure language file path is readable + if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } + } + $this->language = $PHPMAILER_LANG; + + return (bool) $foundlang; // Returns false if language not found + } + + /** + * Get the array of strings for the current language. + * + * @return array + */ + public function getTranslations() + { + return $this->language; + } + + /** + * Create recipient headers. + * + * @param string $type + * @param array $addr An array of recipients, + * where each recipient is a 2-element indexed array with element 0 containing an address + * and element 1 containing a name, like: + * [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']] + * + * @return string + */ + public function addrAppend($type, $addr) + { + $addresses = []; + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); + } + + return $type . ': ' . implode(', ', $addresses) . static::$LE; + } + + /** + * Format an address for use in a message header. + * + * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like + * ['joe@example.com', 'Joe User'] + * + * @return string + */ + public function addrFormat($addr) + { + if (empty($addr[1])) { // No name provided + return $this->secureHeader($addr[0]); + } + + return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader( + $addr[0] + ) . '>'; + } + + /** + * Word-wrap message. + * For use with mailers that do not automatically perform wrapping + * and for quoted-printable encoded messages. + * Original written by philippe. + * + * @param string $message The message to wrap + * @param int $length The line length to wrap to + * @param bool $qp_mode Whether to run in Quoted-Printable mode + * + * @return string + */ + public function wrapText($message, $length, $qp_mode = false) + { + if ($qp_mode) { + $soft_break = sprintf(' =%s', static::$LE); + } else { + $soft_break = static::$LE; + } + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); + $lelen = strlen(static::$LE); + $crlflen = strlen(static::$LE); + + $message = static::normalizeBreaks($message); + //Remove a trailing line break + if (substr($message, -$lelen) == static::$LE) { + $message = substr($message, 0, -$lelen); + } + + //Split message into lines + $lines = explode(static::$LE, $message); + //Message will be rebuilt in here + $message = ''; + foreach ($lines as $line) { + $words = explode(' ', $line); + $buf = ''; + $firstword = true; + foreach ($words as $word) { + if ($qp_mode and (strlen($word) > $length)) { + $space_left = $length - strlen($buf) - $crlflen; + if (!$firstword) { + if ($space_left > 20) { + $len = $space_left; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif ('=' == substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' == substr($word, $len - 2, 1)) { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + $buf .= ' ' . $part; + $message .= $buf . sprintf('=%s', static::$LE); + } else { + $message .= $buf . $soft_break; + } + $buf = ''; + } + while (strlen($word) > 0) { + if ($length <= 0) { + break; + } + $len = $length; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif ('=' == substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' == substr($word, $len - 2, 1)) { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + + if (strlen($word) > 0) { + $message .= $part . sprintf('=%s', static::$LE); + } else { + $buf = $part; + } + } + } else { + $buf_o = $buf; + if (!$firstword) { + $buf .= ' '; + } + $buf .= $word; + + if (strlen($buf) > $length and '' != $buf_o) { + $message .= $buf_o . $soft_break; + $buf = $word; + } + } + $firstword = false; + } + $message .= $buf . static::$LE; + } + + return $message; + } + + /** + * Find the last character boundary prior to $maxLength in a utf-8 + * quoted-printable encoded string. + * Original written by Colin Brown. + * + * @param string $encodedText utf-8 QP text + * @param int $maxLength Find the last character boundary prior to this length + * + * @return int + */ + public function utf8CharBoundary($encodedText, $maxLength) + { + $foundSplitPos = false; + $lookBack = 3; + while (!$foundSplitPos) { + $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); + $encodedCharPos = strpos($lastChunk, '='); + if (false !== $encodedCharPos) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); + $dec = hexdec($hex); + if ($dec < 128) { + // Single byte character. + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + if ($encodedCharPos > 0) { + $maxLength -= $lookBack - $encodedCharPos; + } + $foundSplitPos = true; + } elseif ($dec >= 192) { + // First byte of a multi byte character + // Reduce maxLength to split at start of character + $maxLength -= $lookBack - $encodedCharPos; + $foundSplitPos = true; + } elseif ($dec < 192) { + // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + + return $maxLength; + } + + /** + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. + */ + public function setWordWrap() + { + if ($this->WordWrap < 1) { + return; + } + + switch ($this->message_type) { + case 'alt': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': + $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap); + break; + default: + $this->Body = $this->wrapText($this->Body, $this->WordWrap); + break; + } + } + + /** + * Assemble message headers. + * + * @return string The assembled headers + */ + public function createHeader() + { + $result = ''; + + $result .= $this->headerLine('Date', '' == $this->MessageDate ? self::rfcDate() : $this->MessageDate); + + // To be created automatically by mail() + if ($this->SingleTo) { + if ('mail' != $this->Mailer) { + foreach ($this->to as $toaddr) { + $this->SingleToArray[] = $this->addrFormat($toaddr); + } + } + } else { + if (count($this->to) > 0) { + if ('mail' != $this->Mailer) { + $result .= $this->addrAppend('To', $this->to); + } + } elseif (count($this->cc) == 0) { + $result .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + } + + $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]); + + // sendmail and mail() extract Cc from the header before sending + if (count($this->cc) > 0) { + $result .= $this->addrAppend('Cc', $this->cc); + } + + // sendmail and mail() extract Bcc from the header before sending + if (( + 'sendmail' == $this->Mailer or 'qmail' == $this->Mailer or 'mail' == $this->Mailer + ) + and count($this->bcc) > 0 + ) { + $result .= $this->addrAppend('Bcc', $this->bcc); + } + + if (count($this->ReplyTo) > 0) { + $result .= $this->addrAppend('Reply-To', $this->ReplyTo); + } + + // mail() sets the subject itself + if ('mail' != $this->Mailer) { + $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); + } + + // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) { + $this->lastMessageID = $this->MessageID; + } else { + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); + } + $result .= $this->headerLine('Message-ID', $this->lastMessageID); + if (null !== $this->Priority) { + $result .= $this->headerLine('X-Priority', $this->Priority); + } + if ('' == $this->XMailer) { + $result .= $this->headerLine( + 'X-Mailer', + 'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)' + ); + } else { + $myXmailer = trim($this->XMailer); + if ($myXmailer) { + $result .= $this->headerLine('X-Mailer', $myXmailer); + } + } + + if ('' != $this->ConfirmReadingTo) { + $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); + } + + // Add custom headers + foreach ($this->CustomHeader as $header) { + $result .= $this->headerLine( + trim($header[0]), + $this->encodeHeader(trim($header[1])) + ); + } + if (!$this->sign_key_file) { + $result .= $this->headerLine('MIME-Version', '1.0'); + $result .= $this->getMailMIME(); + } + + return $result; + } + + /** + * Get the message MIME type headers. + * + * @return string + */ + public function getMailMIME() + { + $result = ''; + $ismultipart = true; + switch ($this->message_type) { + case 'inline': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'alt': + case 'alt_inline': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + default: + // Catches case 'plain': and case '': + $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); + $ismultipart = false; + break; + } + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT != $this->Encoding) { + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($ismultipart) { + if (static::ENCODING_8BIT == $this->Encoding) { + $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + } + } + + if ('mail' != $this->Mailer) { + $result .= static::$LE; + } + + return $result; + } + + /** + * Returns the whole MIME message. + * Includes complete headers and body. + * Only valid post preSend(). + * + * @see PHPMailer::preSend() + * + * @return string + */ + public function getSentMIMEMessage() + { + return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody; + } + + /** + * Create a unique ID to use for boundaries. + * + * @return string + */ + protected function generateId() + { + $len = 32; //32 bytes = 256 bits + if (function_exists('random_bytes')) { + $bytes = random_bytes($len); + } elseif (function_exists('openssl_random_pseudo_bytes')) { + $bytes = openssl_random_pseudo_bytes($len); + } else { + //Use a hash to force the length to the same as the other methods + $bytes = hash('sha256', uniqid((string) mt_rand(), true), true); + } + + //We don't care about messing up base64 format here, just want a random string + return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true))); + } + + /** + * Assemble the message body. + * Returns an empty string on failure. + * + * @throws Exception + * + * @return string The assembled message body + */ + public function createBody() + { + $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; + + if ($this->sign_key_file) { + $body .= $this->getMailMIME() . static::$LE; + } + + $this->setWordWrap(); + + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if (static::ENCODING_8BIT == $bodyEncoding and !$this->has8bitChars($this->Body)) { + $bodyEncoding = static::ENCODING_7BIT; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $bodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the body part only + if (static::ENCODING_BASE64 != $this->Encoding and static::hasLineLongerThanMax($this->Body)) { + $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if (static::ENCODING_8BIT == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) { + $altBodyEncoding = static::ENCODING_7BIT; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $altBodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if (static::ENCODING_BASE64 != $altBodyEncoding and static::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; + } + //Use this as a preamble in all multipart message types + $mimepre = 'This is a multi-part message in MIME format.' . static::$LE; + switch ($this->message_type) { + case 'inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[1]); + break; + case 'attach': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + if (!empty($this->Ical)) { + $body .= $this->getBoundary($this->boundary[1], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=REQUEST', ''); + $body .= $this->encodeString($this->Ical, $this->Encoding); + $body .= static::$LE; + } + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= static::$LE; + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + if (!empty($this->Ical)) { + $body .= $this->getBoundary($this->boundary[2], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=REQUEST', ''); + $body .= $this->encodeString($this->Ical, $this->Encoding); + } + $body .= $this->endBoundary($this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt_inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->textLine('--' . $this->boundary[2]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[3]); + $body .= static::$LE; + $body .= $this->endBoundary($this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + default: + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + //Reset the `Encoding` property in case we changed it for line length reasons + $this->Encoding = $bodyEncoding; + $body .= $this->encodeString($this->Body, $this->Encoding); + break; + } + + if ($this->isError()) { + $body = ''; + if ($this->exceptions) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + } elseif ($this->sign_key_file) { + try { + if (!defined('PKCS7_TEXT')) { + throw new Exception($this->lang('extension_missing') . 'openssl'); + } + // @TODO would be nice to use php://temp streams here + $file = tempnam(sys_get_temp_dir(), 'mail'); + if (false === file_put_contents($file, $body)) { + throw new Exception($this->lang('signing') . ' Could not write temp file'); + } + $signed = tempnam(sys_get_temp_dir(), 'signed'); + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if (empty($this->sign_extracerts_file)) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], + [] + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], + [], + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + @unlink($file); + if ($sign) { + $body = file_get_contents($signed); + @unlink($signed); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; + $body = $parts[1]; + } else { + @unlink($signed); + throw new Exception($this->lang('signing') . openssl_error_string()); + } + } catch (Exception $exc) { + $body = ''; + if ($this->exceptions) { + throw $exc; + } + } + } + + return $body; + } + + /** + * Return the start of a message boundary. + * + * @param string $boundary + * @param string $charSet + * @param string $contentType + * @param string $encoding + * + * @return string + */ + protected function getBoundary($boundary, $charSet, $contentType, $encoding) + { + $result = ''; + if ('' == $charSet) { + $charSet = $this->CharSet; + } + if ('' == $contentType) { + $contentType = $this->ContentType; + } + if ('' == $encoding) { + $encoding = $this->Encoding; + } + $result .= $this->textLine('--' . $boundary); + $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); + $result .= static::$LE; + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT != $encoding) { + $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + } + $result .= static::$LE; + + return $result; + } + + /** + * Return the end of a message boundary. + * + * @param string $boundary + * + * @return string + */ + protected function endBoundary($boundary) + { + return static::$LE . '--' . $boundary . '--' . static::$LE; + } + + /** + * Set the message type. + * PHPMailer only supports some preset message types, not arbitrary MIME structures. + */ + protected function setMessageType() + { + $type = []; + if ($this->alternativeExists()) { + $type[] = 'alt'; + } + if ($this->inlineImageExists()) { + $type[] = 'inline'; + } + if ($this->attachmentExists()) { + $type[] = 'attach'; + } + $this->message_type = implode('_', $type); + if ('' == $this->message_type) { + //The 'plain' message_type refers to the message having a single body element, not that it is plain-text + $this->message_type = 'plain'; + } + } + + /** + * Format a header line. + * + * @param string $name + * @param string|int $value + * + * @return string + */ + public function headerLine($name, $value) + { + return $name . ': ' . $value . static::$LE; + } + + /** + * Return a formatted mail line. + * + * @param string $value + * + * @return string + */ + public function textLine($value) + { + return $value . static::$LE; + } + + /** + * Add an attachment from a path on the filesystem. + * Never use a user-supplied path to a file! + * Returns false if the file could not be found or read. + * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client. + * If you need to do that, fetch the resource yourself and pass it in via a local file or string. + * + * @param string $path Path to the attachment + * @param string $name Overrides the attachment name + * @param string $encoding File encoding (see $Encoding) + * @param string $type File extension (MIME) type + * @param string $disposition Disposition to use + * + * @throws Exception + * + * @return bool + */ + public function addAttachment($path, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'attachment') + { + try { + if (!static::isPermittedPath($path) || !@is_file($path)) { + throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + } + + // If a MIME type is not specified, try to work it out from the file name + if ('' == $type) { + $type = static::filenameToType($path); + } + + $filename = basename($path); + if ('' == $name) { + $name = $filename; + } + + $this->attachment[] = [ + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $name, + ]; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + + return true; + } + + /** + * Return the array of attachments. + * + * @return array + */ + public function getAttachments() + { + return $this->attachment; + } + + /** + * Attach all file, string, and binary attachments to the message. + * Returns an empty string on failure. + * + * @param string $disposition_type + * @param string $boundary + * + * @return string + */ + protected function attachAll($disposition_type, $boundary) + { + // Return text of body + $mime = []; + $cidUniq = []; + $incl = []; + + // Add all attachments + foreach ($this->attachment as $attachment) { + // Check if it is a valid disposition_filter + if ($attachment[6] == $disposition_type) { + // Check for string attachment + $string = ''; + $path = ''; + $bString = $attachment[5]; + if ($bString) { + $string = $attachment[0]; + } else { + $path = $attachment[0]; + } + + $inclhash = hash('sha256', serialize($attachment)); + if (in_array($inclhash, $incl)) { + continue; + } + $incl[] = $inclhash; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + if ('inline' == $disposition and array_key_exists($cid, $cidUniq)) { + continue; + } + $cidUniq[$cid] = true; + + $mime[] = sprintf('--%s%s', $boundary, static::$LE); + //Only include a filename property if we have one + if (!empty($name)) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader($this->secureHeader($name)), + static::$LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + static::$LE + ); + } + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT != $encoding) { + $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE); + } + + if (!empty($cid)) { + $mime[] = sprintf('Content-ID: <%s>%s', $cid, static::$LE); + } + + // If a filename contains any of these chars, it should be quoted, + // but not otherwise: RFC2183 & RFC2045 5.1 + // Fixes a warning in IETF's msglint MIME checker + // Allow for bypassing the Content-Disposition header totally + if (!(empty($disposition))) { + $encoded_name = $this->encodeHeader($this->secureHeader($name)); + if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename="%s"%s', + $disposition, + $encoded_name, + static::$LE . static::$LE + ); + } else { + if (!empty($encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + static::$LE . static::$LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + static::$LE . static::$LE + ); + } + } + } else { + $mime[] = static::$LE; + } + + // Encode as string attachment + if ($bString) { + $mime[] = $this->encodeString($string, $encoding); + } else { + $mime[] = $this->encodeFile($path, $encoding); + } + if ($this->isError()) { + return ''; + } + $mime[] = static::$LE; + } + } + + $mime[] = sprintf('--%s--%s', $boundary, static::$LE); + + return implode('', $mime); + } + + /** + * Encode a file attachment in requested format. + * Returns an empty string on failure. + * + * @param string $path The full path to the file + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @throws Exception + * + * @return string + */ + protected function encodeFile($path, $encoding = self::ENCODING_BASE64) + { + try { + if (!static::isPermittedPath($path) || !file_exists($path)) { + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $file_buffer = file_get_contents($path); + if (false === $file_buffer) { + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $file_buffer = $this->encodeString($file_buffer, $encoding); + + return $file_buffer; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + + return ''; + } + } + + /** + * Encode a string in requested format. + * Returns an empty string on failure. + * + * @param string $str The text to encode + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @return string + */ + public function encodeString($str, $encoding = self::ENCODING_BASE64) + { + $encoded = ''; + switch (strtolower($encoding)) { + case static::ENCODING_BASE64: + $encoded = chunk_split( + base64_encode($str), + static::STD_LINE_LENGTH, + static::$LE + ); + break; + case static::ENCODING_7BIT: + case static::ENCODING_8BIT: + $encoded = static::normalizeBreaks($str); + // Make sure it ends with a line break + if (substr($encoded, -(strlen(static::$LE))) != static::$LE) { + $encoded .= static::$LE; + } + break; + case static::ENCODING_BINARY: + $encoded = $str; + break; + case static::ENCODING_QUOTED_PRINTABLE: + $encoded = $this->encodeQP($str); + break; + default: + $this->setError($this->lang('encoding') . $encoding); + break; + } + + return $encoded; + } + + /** + * Encode a header value (not including its label) optimally. + * Picks shortest of Q, B, or none. Result includes folding if needed. + * See RFC822 definitions for phrase, comment and text positions. + * + * @param string $str The header value to encode + * @param string $position What context the string will be used in + * + * @return string + */ + public function encodeHeader($str, $position = 'text') + { + $matchcount = 0; + switch (strtolower($position)) { + case 'phrase': + if (!preg_match('/[\200-\377]/', $str)) { + // Can't use addslashes as we don't know the value of magic_quotes_sybase + $encoded = addcslashes($str, "\0..\37\177\\\""); + if (($str == $encoded) and !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { + return $encoded; + } + + return "\"$encoded\""; + } + $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + break; + /* @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $matchcount = preg_match_all('/[()"]/', $str, $matches); + //fallthrough + case 'text': + default: + $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + break; + } + + //RFCs specify a maximum line length of 78 chars, however mail() will sometimes + //corrupt messages with headers longer than 65 chars. See #818 + $lengthsub = 'mail' == $this->Mailer ? 13 : 0; + $maxlen = static::STD_LINE_LENGTH - $lengthsub; + // Try to select the encoding which should produce the shortest output + if ($matchcount > strlen($str) / 3) { + // More than a third of the content will need encoding, so B encoding will be most efficient + $encoding = 'B'; + //This calculation is: + // max line length + // - shorten to avoid mail() corruption + // - Q/B encoding char overhead ("` =??[QB]??=`") + // - charset name length + $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet); + if ($this->hasMultiBytes($str)) { + // Use a custom function which correctly encodes and wraps long + // multibyte strings without breaking lines within a character + $encoded = $this->base64EncodeWrapMB($str, "\n"); + } else { + $encoded = base64_encode($str); + $maxlen -= $maxlen % 4; + $encoded = trim(chunk_split($encoded, $maxlen, "\n")); + } + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + } elseif ($matchcount > 0) { + //1 or more chars need encoding, use Q-encode + $encoding = 'Q'; + //Recalc max line length for Q encoding - see comments on B encode + $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet); + $encoded = $this->encodeQ($str, $position); + $encoded = $this->wrapText($encoded, $maxlen, true); + $encoded = str_replace('=' . static::$LE, "\n", trim($encoded)); + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + } elseif (strlen($str) > $maxlen) { + //No chars need encoding, but line is too long, so fold it + $encoded = trim($this->wrapText($str, $maxlen, false)); + if ($str == $encoded) { + //Wrapping nicely didn't work, wrap hard instead + $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE)); + } + $encoded = str_replace(static::$LE, "\n", trim($encoded)); + $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded); + } else { + //No reformatting needed + return $str; + } + + return trim(static::normalizeBreaks($encoded)); + } + + /** + * Check if a string contains multi-byte characters. + * + * @param string $str multi-byte text to wrap encode + * + * @return bool + */ + public function hasMultiBytes($str) + { + if (function_exists('mb_strlen')) { + return strlen($str) > mb_strlen($str, $this->CharSet); + } + + // Assume no multibytes (we can't handle without mbstring functions anyway) + return false; + } + + /** + * Does a string contain any 8-bit chars (in any charset)? + * + * @param string $text + * + * @return bool + */ + public function has8bitChars($text) + { + return (bool) preg_match('/[\x80-\xFF]/', $text); + } + + /** + * Encode and wrap long multibyte strings for mail headers + * without breaking lines within a character. + * Adapted from a function by paravoid. + * + * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 + * + * @param string $str multi-byte text to wrap encode + * @param string $linebreak string to use as linefeed/end-of-line + * + * @return string + */ + public function base64EncodeWrapMB($str, $linebreak = null) + { + $start = '=?' . $this->CharSet . '?B?'; + $end = '?='; + $encoded = ''; + if (null === $linebreak) { + $linebreak = static::$LE; + } + + $mb_length = mb_strlen($str, $this->CharSet); + // Each line must have length <= 75, including $start and $end + $length = 75 - strlen($start) - strlen($end); + // Average multi-byte ratio + $ratio = $mb_length / strlen($str); + // Base64 has a 4:3 ratio + $avgLength = floor($length * $ratio * .75); + + for ($i = 0; $i < $mb_length; $i += $offset) { + $lookBack = 0; + do { + $offset = $avgLength - $lookBack; + $chunk = mb_substr($str, $i, $offset, $this->CharSet); + $chunk = base64_encode($chunk); + ++$lookBack; + } while (strlen($chunk) > $length); + $encoded .= $chunk . $linebreak; + } + + // Chomp the last linefeed + return substr($encoded, 0, -strlen($linebreak)); + } + + /** + * Encode a string in quoted-printable format. + * According to RFC2045 section 6.7. + * + * @param string $string The text to encode + * + * @return string + */ + public function encodeQP($string) + { + return static::normalizeBreaks(quoted_printable_encode($string)); + } + + /** + * Encode a string using Q encoding. + * + * @see http://tools.ietf.org/html/rfc2047#section-4.2 + * + * @param string $str the text to encode + * @param string $position Where the text is going to be used, see the RFC for what that means + * + * @return string + */ + public function encodeQ($str, $position = 'text') + { + // There should not be any EOL in the string + $pattern = ''; + $encoded = str_replace(["\r", "\n"], '', $str); + switch (strtolower($position)) { + case 'phrase': + // RFC 2047 section 5.3 + $pattern = '^A-Za-z0-9!*+\/ -'; + break; + /* + * RFC 2047 section 5.2. + * Build $pattern without including delimiters and [] + */ + /* @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $pattern = '\(\)"'; + /* Intentional fall through */ + case 'text': + default: + // RFC 2047 section 5.1 + // Replace every high ascii, control, =, ? and _ characters + /** @noinspection SuspiciousAssignmentsInspection */ + $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; + break; + } + $matches = []; + if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { + // If the string contains an '=', make sure it's the first thing we replace + // so as to avoid double-encoding + $eqkey = array_search('=', $matches[0]); + if (false !== $eqkey) { + unset($matches[0][$eqkey]); + array_unshift($matches[0], '='); + } + foreach (array_unique($matches[0]) as $char) { + $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); + } + } + // Replace spaces with _ (more readable than =20) + // RFC 2047 section 4.2(2) + return str_replace(' ', '_', $encoded); + } + + /** + * Add a string or binary attachment (non-filesystem). + * This method can be used to attach ascii or binary data, + * such as a BLOB record from a database. + * + * @param string $string String attachment data + * @param string $filename Name of the attachment + * @param string $encoding File encoding (see $Encoding) + * @param string $type File extension (MIME) type + * @param string $disposition Disposition to use + */ + public function addStringAttachment( + $string, + $filename, + $encoding = self::ENCODING_BASE64, + $type = '', + $disposition = 'attachment' + ) { + // If a MIME type is not specified, try to work it out from the file name + if ('' == $type) { + $type = static::filenameToType($filename); + } + // Append to $attachment array + $this->attachment[] = [ + 0 => $string, + 1 => $filename, + 2 => basename($filename), + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => 0, + ]; + } + + /** + * Add an embedded (inline) attachment from a file. + * This can include images, sounds, and just about any other document type. + * These differ from 'regular' attachments in that they are intended to be + * displayed inline with the message, not just attached for download. + * This is used in HTML messages that embed the images + * the HTML refers to using the $cid value. + * Never use a user-supplied path to a file! + * + * @param string $path Path to the attachment + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML + * @param string $name Overrides the attachment name + * @param string $encoding File encoding (see $Encoding) + * @param string $type File MIME type + * @param string $disposition Disposition to use + * + * @return bool True on successfully adding an attachment + */ + public function addEmbeddedImage($path, $cid, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'inline') + { + if (!static::isPermittedPath($path) || !@is_file($path)) { + $this->setError($this->lang('file_access') . $path); + + return false; + } + + // If a MIME type is not specified, try to work it out from the file name + if ('' == $type) { + $type = static::filenameToType($path); + } + + $filename = basename($path); + if ('' == $name) { + $name = $filename; + } + + // Append to $attachment array + $this->attachment[] = [ + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $cid, + ]; + + return true; + } + + /** + * Add an embedded stringified attachment. + * This can include images, sounds, and just about any other document type. + * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type. + * + * @param string $string The attachment binary data + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML + * @param string $name A filename for the attachment. If this contains an extension, + * PHPMailer will attempt to set a MIME type for the attachment. + * For example 'file.jpg' would get an 'image/jpeg' MIME type. + * @param string $encoding File encoding (see $Encoding), defaults to 'base64' + * @param string $type MIME type - will be used in preference to any automatically derived type + * @param string $disposition Disposition to use + * + * @return bool True on successfully adding an attachment + */ + public function addStringEmbeddedImage( + $string, + $cid, + $name = '', + $encoding = self::ENCODING_BASE64, + $type = '', + $disposition = 'inline' + ) { + // If a MIME type is not specified, try to work it out from the name + if ('' == $type and !empty($name)) { + $type = static::filenameToType($name); + } + + // Append to $attachment array + $this->attachment[] = [ + 0 => $string, + 1 => $name, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => $cid, + ]; + + return true; + } + + /** + * Check if an embedded attachment is present with this cid. + * + * @param string $cid + * + * @return bool + */ + protected function cidExists($cid) + { + foreach ($this->attachment as $attachment) { + if ('inline' == $attachment[6] and $cid == $attachment[7]) { + return true; + } + } + + return false; + } + + /** + * Check if an inline attachment is present. + * + * @return bool + */ + public function inlineImageExists() + { + foreach ($this->attachment as $attachment) { + if ('inline' == $attachment[6]) { + return true; + } + } + + return false; + } + + /** + * Check if an attachment (non-inline) is present. + * + * @return bool + */ + public function attachmentExists() + { + foreach ($this->attachment as $attachment) { + if ('attachment' == $attachment[6]) { + return true; + } + } + + return false; + } + + /** + * Check if this message has an alternative body set. + * + * @return bool + */ + public function alternativeExists() + { + return !empty($this->AltBody); + } + + /** + * Clear queued addresses of given kind. + * + * @param string $kind 'to', 'cc', or 'bcc' + */ + public function clearQueuedAddresses($kind) + { + $this->RecipientsQueue = array_filter( + $this->RecipientsQueue, + function ($params) use ($kind) { + return $params[0] != $kind; + } + ); + } + + /** + * Clear all To recipients. + */ + public function clearAddresses() + { + foreach ($this->to as $to) { + unset($this->all_recipients[strtolower($to[0])]); + } + $this->to = []; + $this->clearQueuedAddresses('to'); + } + + /** + * Clear all CC recipients. + */ + public function clearCCs() + { + foreach ($this->cc as $cc) { + unset($this->all_recipients[strtolower($cc[0])]); + } + $this->cc = []; + $this->clearQueuedAddresses('cc'); + } + + /** + * Clear all BCC recipients. + */ + public function clearBCCs() + { + foreach ($this->bcc as $bcc) { + unset($this->all_recipients[strtolower($bcc[0])]); + } + $this->bcc = []; + $this->clearQueuedAddresses('bcc'); + } + + /** + * Clear all ReplyTo recipients. + */ + public function clearReplyTos() + { + $this->ReplyTo = []; + $this->ReplyToQueue = []; + } + + /** + * Clear all recipient types. + */ + public function clearAllRecipients() + { + $this->to = []; + $this->cc = []; + $this->bcc = []; + $this->all_recipients = []; + $this->RecipientsQueue = []; + } + + /** + * Clear all filesystem, string, and binary attachments. + */ + public function clearAttachments() + { + $this->attachment = []; + } + + /** + * Clear all custom headers. + */ + public function clearCustomHeaders() + { + $this->CustomHeader = []; + } + + /** + * Add an error message to the error container. + * + * @param string $msg + */ + protected function setError($msg) + { + ++$this->error_count; + if ('smtp' == $this->Mailer and null !== $this->smtp) { + $lasterror = $this->smtp->getError(); + if (!empty($lasterror['error'])) { + $msg .= $this->lang('smtp_error') . $lasterror['error']; + if (!empty($lasterror['detail'])) { + $msg .= ' Detail: ' . $lasterror['detail']; + } + if (!empty($lasterror['smtp_code'])) { + $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + } + if (!empty($lasterror['smtp_code_ex'])) { + $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + } + } + } + $this->ErrorInfo = $msg; + } + + /** + * Return an RFC 822 formatted date. + * + * @return string + */ + public static function rfcDate() + { + // Set the time zone to whatever the default is to avoid 500 errors + // Will default to UTC if it's not set properly in php.ini + date_default_timezone_set(@date_default_timezone_get()); + + return date('D, j M Y H:i:s O'); + } + + /** + * Get the server hostname. + * Returns 'localhost.localdomain' if unknown. + * + * @return string + */ + protected function serverHostname() + { + $result = ''; + if (!empty($this->Hostname)) { + $result = $this->Hostname; + } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER)) { + $result = $_SERVER['SERVER_NAME']; + } elseif (function_exists('gethostname') and gethostname() !== false) { + $result = gethostname(); + } elseif (php_uname('n') !== false) { + $result = php_uname('n'); + } + if (!static::isValidHost($result)) { + return 'localhost.localdomain'; + } + + return $result; + } + + /** + * Validate whether a string contains a valid value to use as a hostname or IP address. + * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`. + * + * @param string $host The host name or IP address to check + * + * @return bool + */ + public static function isValidHost($host) + { + //Simple syntax limits + if (empty($host) + or !is_string($host) + or strlen($host) > 256 + ) { + return false; + } + //Looks like a bracketed IPv6 address + if (trim($host, '[]') != $host) { + return (bool) filter_var(trim($host, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + } + //If removing all the dots results in a numeric string, it must be an IPv4 address. + //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names + if (is_numeric(str_replace('.', '', $host))) { + //Is it a valid IPv4 address? + return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + if (filter_var('http://' . $host, FILTER_VALIDATE_URL)) { + //Is it a syntactically valid hostname? + return true; + } + + return false; + } + + /** + * Get an error message in the current language. + * + * @param string $key + * + * @return string + */ + protected function lang($key) + { + if (count($this->language) < 1) { + $this->setLanguage('en'); // set the default language + } + + if (array_key_exists($key, $this->language)) { + if ('smtp_connect_failed' == $key) { + //Include a link to troubleshooting docs on SMTP connection failure + //this is by far the biggest cause of support questions + //but it's usually not PHPMailer's fault. + return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + } + + return $this->language[$key]; + } + + //Return the key as a fallback + return $key; + } + + /** + * Check if an error occurred. + * + * @return bool True if an error did occur + */ + public function isError() + { + return $this->error_count > 0; + } + + /** + * Add a custom header. + * $name value can be overloaded to contain + * both header name and value (name:value). + * + * @param string $name Custom header name + * @param string|null $value Header value + */ + public function addCustomHeader($name, $value = null) + { + if (null === $value) { + // Value passed in as name:value + $this->CustomHeader[] = explode(':', $name, 2); + } else { + $this->CustomHeader[] = [$name, $value]; + } + } + + /** + * Returns all custom headers. + * + * @return array + */ + public function getCustomHeaders() + { + return $this->CustomHeader; + } + + /** + * Create a message body from an HTML string. + * Automatically inlines images and creates a plain-text version by converting the HTML, + * overwriting any existing values in Body and AltBody. + * Do not source $message content from user input! + * $basedir is prepended when handling relative URLs, e.g. and must not be empty + * will look for an image file in $basedir/images/a.png and convert it to inline. + * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email) + * Converts data-uri images into embedded attachments. + * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly. + * + * @param string $message HTML message string + * @param string $basedir Absolute path to a base directory to prepend to relative paths to images + * @param bool|callable $advanced Whether to use the internal HTML to text converter + * or your own custom converter @see PHPMailer::html2text() + * + * @return string $message The transformed message Body + */ + public function msgHTML($message, $basedir = '', $advanced = false) + { + preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); + if (array_key_exists(2, $images)) { + if (strlen($basedir) > 1 && '/' != substr($basedir, -1)) { + // Ensure $basedir has a trailing / + $basedir .= '/'; + } + foreach ($images[2] as $imgindex => $url) { + // Convert data URIs into embedded images + //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) { + if (count($match) == 4 and static::ENCODING_BASE64 == $match[2]) { + $data = base64_decode($match[3]); + } elseif ('' == $match[2]) { + $data = rawurldecode($match[3]); + } else { + //Not recognised so leave it alone + continue; + } + //Hash the decoded data, not the URL so that the same data-URI image used in multiple places + //will only be embedded once, even if it used a different encoding + $cid = hash('sha256', $data) . '@phpmailer.0'; // RFC2392 S 2 + + if (!$this->cidExists($cid)) { + $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, static::ENCODING_BASE64, $match[1]); + } + $message = str_replace( + $images[0][$imgindex], + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + continue; + } + if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths) + !empty($basedir) + // Ignore URLs containing parent dir traversal (..) + and (strpos($url, '..') === false) + // Do not change urls that are already inline images + and 0 !== strpos($url, 'cid:') + // Do not change absolute URLs, including anonymous protocol + and !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) + ) { + $filename = basename($url); + $directory = dirname($url); + if ('.' == $directory) { + $directory = ''; + } + $cid = hash('sha256', $url) . '@phpmailer.0'; // RFC2392 S 2 + if (strlen($basedir) > 1 and '/' != substr($basedir, -1)) { + $basedir .= '/'; + } + if (strlen($directory) > 1 and '/' != substr($directory, -1)) { + $directory .= '/'; + } + if ($this->addEmbeddedImage( + $basedir . $directory . $filename, + $cid, + $filename, + static::ENCODING_BASE64, + static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION)) + ) + ) { + $message = preg_replace( + '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + } + } + } + } + $this->isHTML(true); + // Convert all message body line breaks to LE, makes quoted-printable encoding work much better + $this->Body = static::normalizeBreaks($message); + $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced)); + if (!$this->alternativeExists()) { + $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.' + . static::$LE; + } + + return $this->Body; + } + + /** + * Convert an HTML string into plain text. + * This is used by msgHTML(). + * Note - older versions of this function used a bundled advanced converter + * which was removed for license reasons in #232. + * Example usage: + * + * ```php + * // Use default conversion + * $plain = $mail->html2text($html); + * // Use your own custom converter + * $plain = $mail->html2text($html, function($html) { + * $converter = new MyHtml2text($html); + * return $converter->get_text(); + * }); + * ``` + * + * @param string $html The HTML text to convert + * @param bool|callable $advanced Any boolean value to use the internal converter, + * or provide your own callable for custom conversion + * + * @return string + */ + public function html2text($html, $advanced = false) + { + if (is_callable($advanced)) { + return call_user_func($advanced, $html); + } + + return html_entity_decode( + trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), + ENT_QUOTES, + $this->CharSet + ); + } + + /** + * Get the MIME type for a file extension. + * + * @param string $ext File extension + * + * @return string MIME type of file + */ + public static function _mime_types($ext = '') + { + $mimes = [ + 'xl' => 'application/excel', + 'js' => 'application/javascript', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'bin' => 'application/macbinary', + 'doc' => 'application/msword', + 'word' => 'application/msword', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'class' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'psd' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'so' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'zip' => 'application/zip', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'm4a' => 'audio/mp4', + 'mpga' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'wav' => 'audio/x-wav', + 'mka' => 'audio/x-matroska', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'webp' => 'image/webp', + 'heif' => 'image/heif', + 'heifs' => 'image/heif-sequence', + 'heic' => 'image/heic', + 'heics' => 'image/heic-sequence', + 'eml' => 'message/rfc822', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'log' => 'text/plain', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'ics' => 'text/calendar', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'wmv' => 'video/x-ms-wmv', + 'mpeg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mp4' => 'video/mp4', + 'm4v' => 'video/mp4', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'rv' => 'video/vnd.rn-realvideo', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'webm' => 'video/webm', + 'mkv' => 'video/x-matroska', + ]; + $ext = strtolower($ext); + if (array_key_exists($ext, $mimes)) { + return $mimes[$ext]; + } + + return 'application/octet-stream'; + } + + /** + * Map a file name to a MIME type. + * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. + * + * @param string $filename A file name or full path, does not need to exist as a file + * + * @return string + */ + public static function filenameToType($filename) + { + // In case the path is a URL, strip any query string before getting extension + $qpos = strpos($filename, '?'); + if (false !== $qpos) { + $filename = substr($filename, 0, $qpos); + } + $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION); + + return static::_mime_types($ext); + } + + /** + * Multi-byte-safe pathinfo replacement. + * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe. + * + * @see http://www.php.net/manual/en/function.pathinfo.php#107461 + * + * @param string $path A filename or path, does not need to exist as a file + * @param int|string $options Either a PATHINFO_* constant, + * or a string name to return only the specified piece + * + * @return string|array + */ + public static function mb_pathinfo($path, $options = null) + { + $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '']; + $pathinfo = []; + if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) { + if (array_key_exists(1, $pathinfo)) { + $ret['dirname'] = $pathinfo[1]; + } + if (array_key_exists(2, $pathinfo)) { + $ret['basename'] = $pathinfo[2]; + } + if (array_key_exists(5, $pathinfo)) { + $ret['extension'] = $pathinfo[5]; + } + if (array_key_exists(3, $pathinfo)) { + $ret['filename'] = $pathinfo[3]; + } + } + switch ($options) { + case PATHINFO_DIRNAME: + case 'dirname': + return $ret['dirname']; + case PATHINFO_BASENAME: + case 'basename': + return $ret['basename']; + case PATHINFO_EXTENSION: + case 'extension': + return $ret['extension']; + case PATHINFO_FILENAME: + case 'filename': + return $ret['filename']; + default: + return $ret; + } + } + + /** + * Set or reset instance properties. + * You should avoid this function - it's more verbose, less efficient, more error-prone and + * harder to debug than setting properties directly. + * Usage Example: + * `$mail->set('SMTPSecure', 'tls');` + * is the same as: + * `$mail->SMTPSecure = 'tls';`. + * + * @param string $name The property name to set + * @param mixed $value The value to set the property to + * + * @return bool + */ + public function set($name, $value = '') + { + if (property_exists($this, $name)) { + $this->$name = $value; + + return true; + } + $this->setError($this->lang('variable_set') . $name); + + return false; + } + + /** + * Strip newlines to prevent header injection. + * + * @param string $str + * + * @return string + */ + public function secureHeader($str) + { + return trim(str_replace(["\r", "\n"], '', $str)); + } + + /** + * Normalize line breaks in a string. + * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format. + * Defaults to CRLF (for message bodies) and preserves consecutive breaks. + * + * @param string $text + * @param string $breaktype What kind of line break to use; defaults to static::$LE + * + * @return string + */ + public static function normalizeBreaks($text, $breaktype = null) + { + if (null === $breaktype) { + $breaktype = static::$LE; + } + // Normalise to \n + $text = str_replace(["\r\n", "\r"], "\n", $text); + // Now convert LE as needed + if ("\n" !== $breaktype) { + $text = str_replace("\n", $breaktype, $text); + } + + return $text; + } + + /** + * Return the current line break format string. + * + * @return string + */ + public static function getLE() + { + return static::$LE; + } + + /** + * Set the line break format string, e.g. "\r\n". + * + * @param string $le + */ + protected static function setLE($le) + { + static::$LE = $le; + } + + /** + * Set the public and private key files and password for S/MIME signing. + * + * @param string $cert_filename + * @param string $key_filename + * @param string $key_pass Password for private key + * @param string $extracerts_filename Optional path to chain certificate + */ + public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') + { + $this->sign_cert_file = $cert_filename; + $this->sign_key_file = $key_filename; + $this->sign_key_pass = $key_pass; + $this->sign_extracerts_file = $extracerts_filename; + } + + /** + * Quoted-Printable-encode a DKIM header. + * + * @param string $txt + * + * @return string + */ + public function DKIM_QP($txt) + { + $line = ''; + $len = strlen($txt); + for ($i = 0; $i < $len; ++$i) { + $ord = ord($txt[$i]); + if (((0x21 <= $ord) and ($ord <= 0x3A)) or $ord == 0x3C or ((0x3E <= $ord) and ($ord <= 0x7E))) { + $line .= $txt[$i]; + } else { + $line .= '=' . sprintf('%02X', $ord); + } + } + + return $line; + } + + /** + * Generate a DKIM signature. + * + * @param string $signHeader + * + * @throws Exception + * + * @return string The DKIM signature value + */ + public function DKIM_Sign($signHeader) + { + if (!defined('PKCS7_TEXT')) { + if ($this->exceptions) { + throw new Exception($this->lang('extension_missing') . 'openssl'); + } + + return ''; + } + $privKeyStr = !empty($this->DKIM_private_string) ? + $this->DKIM_private_string : + file_get_contents($this->DKIM_private); + if ('' != $this->DKIM_passphrase) { + $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); + } else { + $privKey = openssl_pkey_get_private($privKeyStr); + } + if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { + openssl_pkey_free($privKey); + + return base64_encode($signature); + } + openssl_pkey_free($privKey); + + return ''; + } + + /** + * Generate a DKIM canonicalization header. + * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2. + * Canonicalized headers should *always* use CRLF, regardless of mailer setting. + * + * @see https://tools.ietf.org/html/rfc6376#section-3.4.2 + * + * @param string $signHeader Header + * + * @return string + */ + public function DKIM_HeaderC($signHeader) + { + //Unfold all header continuation lines + //Also collapses folded whitespace. + //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]` + //@see https://tools.ietf.org/html/rfc5322#section-2.2 + //That means this may break if you do something daft like put vertical tabs in your headers. + $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader); + $lines = explode("\r\n", $signHeader); + foreach ($lines as $key => $line) { + //If the header is missing a :, skip it as it's invalid + //This is likely to happen because the explode() above will also split + //on the trailing LE, leaving an empty line + if (strpos($line, ':') === false) { + continue; + } + list($heading, $value) = explode(':', $line, 2); + //Lower-case header name + $heading = strtolower($heading); + //Collapse white space within the value + $value = preg_replace('/[ \t]{2,}/', ' ', $value); + //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value + //But then says to delete space before and after the colon. + //Net result is the same as trimming both ends of the value. + //by elimination, the same applies to the field name + $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t"); + } + + return implode("\r\n", $lines); + } + + /** + * Generate a DKIM canonicalization body. + * Uses the 'simple' algorithm from RFC6376 section 3.4.3. + * Canonicalized bodies should *always* use CRLF, regardless of mailer setting. + * + * @see https://tools.ietf.org/html/rfc6376#section-3.4.3 + * + * @param string $body Message Body + * + * @return string + */ + public function DKIM_BodyC($body) + { + if (empty($body)) { + return "\r\n"; + } + // Normalize line endings to CRLF + $body = static::normalizeBreaks($body, "\r\n"); + + //Reduce multiple trailing line breaks to a single one + return rtrim($body, "\r\n") . "\r\n"; + } + + /** + * Create the DKIM header and body in a new message header. + * + * @param string $headers_line Header lines + * @param string $subject Subject + * @param string $body Body + * + * @return string + */ + public function DKIM_Add($headers_line, $subject, $body) + { + $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms + $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body + $DKIMquery = 'dns/txt'; // Query method + $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) + $subject_header = "Subject: $subject"; + $headers = explode(static::$LE, $headers_line); + $from_header = ''; + $to_header = ''; + $date_header = ''; + $current = ''; + $copiedHeaderFields = ''; + $foundExtraHeaders = []; + $extraHeaderKeys = ''; + $extraHeaderValues = ''; + $extraCopyHeaderFields = ''; + foreach ($headers as $header) { + if (strpos($header, 'From:') === 0) { + $from_header = $header; + $current = 'from_header'; + } elseif (strpos($header, 'To:') === 0) { + $to_header = $header; + $current = 'to_header'; + } elseif (strpos($header, 'Date:') === 0) { + $date_header = $header; + $current = 'date_header'; + } elseif (!empty($this->DKIM_extraHeaders)) { + foreach ($this->DKIM_extraHeaders as $extraHeader) { + if (strpos($header, $extraHeader . ':') === 0) { + $headerValue = $header; + foreach ($this->CustomHeader as $customHeader) { + if ($customHeader[0] === $extraHeader) { + $headerValue = trim($customHeader[0]) . + ': ' . + $this->encodeHeader(trim($customHeader[1])); + break; + } + } + $foundExtraHeaders[$extraHeader] = $headerValue; + $current = ''; + break; + } + } + } else { + if (!empty($$current) and strpos($header, ' =?') === 0) { + $$current .= $header; + } else { + $current = ''; + } + } + } + foreach ($foundExtraHeaders as $key => $value) { + $extraHeaderKeys .= ':' . $key; + $extraHeaderValues .= $value . "\r\n"; + if ($this->DKIM_copyHeaderFields) { + $extraCopyHeaderFields .= "\t|" . str_replace('|', '=7C', $this->DKIM_QP($value)) . ";\r\n"; + } + } + if ($this->DKIM_copyHeaderFields) { + $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); + $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); + $date = str_replace('|', '=7C', $this->DKIM_QP($date_header)); + $subject = str_replace('|', '=7C', $this->DKIM_QP($subject_header)); + $copiedHeaderFields = "\tz=$from\r\n" . + "\t|$to\r\n" . + "\t|$date\r\n" . + "\t|$subject;\r\n" . + $extraCopyHeaderFields; + } + $body = $this->DKIM_BodyC($body); + $DKIMlen = strlen($body); // Length of body + $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body + if ('' == $this->DKIM_identity) { + $ident = ''; + } else { + $ident = ' i=' . $this->DKIM_identity . ';'; + } + $dkimhdrs = 'DKIM-Signature: v=1; a=' . + $DKIMsignatureType . '; q=' . + $DKIMquery . '; l=' . + $DKIMlen . '; s=' . + $this->DKIM_selector . + ";\r\n" . + "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . + "\th=From:To:Date:Subject" . $extraHeaderKeys . ";\r\n" . + "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . + $copiedHeaderFields . + "\tbh=" . $DKIMb64 . ";\r\n" . + "\tb="; + $toSign = $this->DKIM_HeaderC( + $from_header . "\r\n" . + $to_header . "\r\n" . + $date_header . "\r\n" . + $subject_header . "\r\n" . + $extraHeaderValues . + $dkimhdrs + ); + $signed = $this->DKIM_Sign($toSign); + + return static::normalizeBreaks($dkimhdrs . $signed) . static::$LE; + } + + /** + * Detect if a string contains a line longer than the maximum line length + * allowed by RFC 2822 section 2.1.1. + * + * @param string $str + * + * @return bool + */ + public static function hasLineLongerThanMax($str) + { + return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str); + } + + /** + * Allows for public read access to 'to' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getToAddresses() + { + return $this->to; + } + + /** + * Allows for public read access to 'cc' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getCcAddresses() + { + return $this->cc; + } + + /** + * Allows for public read access to 'bcc' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getBccAddresses() + { + return $this->bcc; + } + + /** + * Allows for public read access to 'ReplyTo' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getReplyToAddresses() + { + return $this->ReplyTo; + } + + /** + * Allows for public read access to 'all_recipients' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getAllRecipientAddresses() + { + return $this->all_recipients; + } + + /** + * Perform a callback. + * + * @param bool $isSent + * @param array $to + * @param array $cc + * @param array $bcc + * @param string $subject + * @param string $body + * @param string $from + * @param array $extra + */ + protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra) + { + if (!empty($this->action_function) and is_callable($this->action_function)) { + call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra); + } + } + + /** + * Get the OAuth instance. + * + * @return OAuth + */ + public function getOAuth() + { + return $this->oauth; + } + + /** + * Set an OAuth instance. + * + * @param OAuth $oauth + */ + public function setOAuth(OAuth $oauth) + { + $this->oauth = $oauth; + } +} diff --git a/lib/PHPMailer/PHPMailer/SMTP.php b/lib/PHPMailer/PHPMailer/SMTP.php new file mode 100644 index 000000000..da85442bf --- /dev/null +++ b/lib/PHPMailer/PHPMailer/SMTP.php @@ -0,0 +1,1326 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer RFC821 SMTP email transport class. + * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. + * + * @author Chris Ryan + * @author Marcus Bointon + */ +class SMTP +{ + /** + * The PHPMailer SMTP version number. + * + * @var string + */ + const VERSION = '6.0.7'; + + /** + * SMTP line break constant. + * + * @var string + */ + const LE = "\r\n"; + + /** + * The SMTP port to use if one is not specified. + * + * @var int + */ + const DEFAULT_PORT = 25; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * Debug level for no output. + */ + const DEBUG_OFF = 0; + + /** + * Debug level to show client -> server messages. + */ + const DEBUG_CLIENT = 1; + + /** + * Debug level to show client -> server and server -> client messages. + */ + const DEBUG_SERVER = 2; + + /** + * Debug level to show connection status, client -> server and server -> client messages. + */ + const DEBUG_CONNECTION = 3; + + /** + * Debug level to show all messages. + */ + const DEBUG_LOWLEVEL = 4; + + /** + * Debug output level. + * Options: + * * self::DEBUG_OFF (`0`) No debug output, default + * * self::DEBUG_CLIENT (`1`) Client commands + * * self::DEBUG_SERVER (`2`) Client commands and server responses + * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status + * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages. + * + * @var int + */ + public $do_debug = self::DEBUG_OFF; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to use VERP. + * + * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Info on VERP + * + * @var bool + */ + public $do_verp = false; + + /** + * The timeout value for connection, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. + * + * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 + * + * @var int + */ + public $Timeout = 300; + + /** + * How long to wait for commands to complete, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timelimit = 300; + + /** + * Patterns to extract an SMTP transaction id from reply to a DATA command. + * The first capture group in each regex will be used as the ID. + * MS ESMTP returns the message ID, which may not be correct for internal tracking. + * + * @var string[] + */ + protected $smtp_transaction_id_patterns = [ + 'exim' => '/[\d]{3} OK id=(.*)/', + 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', + 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', + 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', + 'Amazon_SES' => '/[\d]{3} Ok (.*)/', + 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', + 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', + ]; + + /** + * The last transaction ID issued in response to a DATA command, + * if one was detected. + * + * @var string|bool|null + */ + protected $last_smtp_transaction_id; + + /** + * The socket for the server connection. + * + * @var ?resource + */ + protected $smtp_conn; + + /** + * Error information, if any, for the last SMTP command. + * + * @var array + */ + protected $error = [ + 'error' => '', + 'detail' => '', + 'smtp_code' => '', + 'smtp_code_ex' => '', + ]; + + /** + * The reply the server sent to us for HELO. + * If null, no HELO string has yet been received. + * + * @var string|null + */ + protected $helo_rply = null; + + /** + * The set of SMTP extensions sent in reply to EHLO command. + * Indexes of the array are extension names. + * Value at index 'HELO' or 'EHLO' (according to command that was sent) + * represents the server name. In case of HELO it is the only element of the array. + * Other values can be boolean TRUE or an array containing extension options. + * If null, no HELO/EHLO string has yet been received. + * + * @var array|null + */ + protected $server_caps = null; + + /** + * The most recent reply received from the server. + * + * @var string + */ + protected $last_reply = ''; + + /** + * Output debugging info via a user-selected method. + * + * @param string $str Debug string to output + * @param int $level The debug level of this message; see DEBUG_* constants + * + * @see SMTP::$Debugoutput + * @see SMTP::$do_debug + */ + protected function edebug($str, $level = 0) + { + if ($level > $this->do_debug) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $level); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Connect to an SMTP server. + * + * @param string $host SMTP server IP or host name + * @param int $port The port number to connect to + * @param int $timeout How long to wait for the connection to open + * @param array $options An array of options for stream_context_create() + * + * @return bool + */ + public function connect($host, $port = null, $timeout = 30, $options = []) + { + static $streamok; + //This is enabled by default since 5.0.0 but some providers disable it + //Check this once and cache the result + if (null === $streamok) { + $streamok = function_exists('stream_socket_client'); + } + // Clear errors to avoid confusion + $this->setError(''); + // Make sure we are __not__ connected + if ($this->connected()) { + // Already connected, generate error + $this->setError('Already connected to a server'); + + return false; + } + if (empty($port)) { + $port = self::DEFAULT_PORT; + } + // Connect to the SMTP server + $this->edebug( + "Connection: opening to $host:$port, timeout=$timeout, options=" . + (count($options) > 0 ? var_export($options, true) : 'array()'), + self::DEBUG_CONNECTION + ); + $errno = 0; + $errstr = ''; + if ($streamok) { + $socket_context = stream_context_create($options); + set_error_handler([$this, 'errorHandler']); + $this->smtp_conn = stream_socket_client( + $host . ':' . $port, + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT, + $socket_context + ); + restore_error_handler(); + } else { + //Fall back to fsockopen which should work in more places, but is missing some features + $this->edebug( + 'Connection: stream_socket_client not available, falling back to fsockopen', + self::DEBUG_CONNECTION + ); + set_error_handler([$this, 'errorHandler']); + $this->smtp_conn = fsockopen( + $host, + $port, + $errno, + $errstr, + $timeout + ); + restore_error_handler(); + } + // Verify we connected properly + if (!is_resource($this->smtp_conn)) { + $this->setError( + 'Failed to connect to server', + '', + (string) $errno, + (string) $errstr + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] + . ": $errstr ($errno)", + self::DEBUG_CLIENT + ); + + return false; + } + $this->edebug('Connection: opened', self::DEBUG_CONNECTION); + // SMTP server can take longer to respond, give longer timeout for first read + // Windows does not have support for this timeout function + if (substr(PHP_OS, 0, 3) != 'WIN') { + $max = ini_get('max_execution_time'); + // Don't bother if unlimited + if (0 != $max and $timeout > $max) { + @set_time_limit($timeout); + } + stream_set_timeout($this->smtp_conn, $timeout, 0); + } + // Get any announcement + $announce = $this->get_lines(); + $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER); + + return true; + } + + /** + * Initiate a TLS (encrypted) session. + * + * @return bool + */ + public function startTLS() + { + if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { + return false; + } + + //Allow the best TLS version(s) we can + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT + //so add them back in manually if we can + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + + // Begin encrypted connection + set_error_handler([$this, 'errorHandler']); + $crypto_ok = stream_socket_enable_crypto( + $this->smtp_conn, + true, + $crypto_method + ); + restore_error_handler(); + + return (bool) $crypto_ok; + } + + /** + * Perform SMTP authentication. + * Must be run after hello(). + * + * @see hello() + * + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2) + * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication + * + * @return bool True if successfully authenticated + */ + public function authenticate( + $username, + $password, + $authtype = null, + $OAuth = null + ) { + if (!$this->server_caps) { + $this->setError('Authentication is not allowed before HELO/EHLO'); + + return false; + } + + if (array_key_exists('EHLO', $this->server_caps)) { + // SMTP extensions are available; try to find a proper authentication method + if (!array_key_exists('AUTH', $this->server_caps)) { + $this->setError('Authentication is not allowed at this stage'); + // 'at this stage' means that auth may be allowed after the stage changes + // e.g. after STARTTLS + + return false; + } + + $this->edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNSPECIFIED'), self::DEBUG_LOWLEVEL); + $this->edebug( + 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), + self::DEBUG_LOWLEVEL + ); + + //If we have requested a specific auth type, check the server supports it before trying others + if (null !== $authtype and !in_array($authtype, $this->server_caps['AUTH'])) { + $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL); + $authtype = null; + } + + if (empty($authtype)) { + //If no auth mechanism is specified, attempt to use these, in this order + //Try CRAM-MD5 first as it's more secure than the others + foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) { + if (in_array($method, $this->server_caps['AUTH'])) { + $authtype = $method; + break; + } + } + if (empty($authtype)) { + $this->setError('No supported authentication methods found'); + + return false; + } + self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); + } + + if (!in_array($authtype, $this->server_caps['AUTH'])) { + $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); + + return false; + } + } elseif (empty($authtype)) { + $authtype = 'LOGIN'; + } + switch ($authtype) { + case 'PLAIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { + return false; + } + // Send encoded username and password + if (!$this->sendCommand( + 'User & Password', + base64_encode("\0" . $username . "\0" . $password), + 235 + ) + ) { + return false; + } + break; + case 'LOGIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { + return false; + } + if (!$this->sendCommand('Username', base64_encode($username), 334)) { + return false; + } + if (!$this->sendCommand('Password', base64_encode($password), 235)) { + return false; + } + break; + case 'CRAM-MD5': + // Start authentication + if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { + return false; + } + // Get the challenge + $challenge = base64_decode(substr($this->last_reply, 4)); + + // Build the response + $response = $username . ' ' . $this->hmac($challenge, $password); + + // send encoded credentials + return $this->sendCommand('Username', base64_encode($response), 235); + case 'XOAUTH2': + //The OAuth instance must be set up prior to requesting auth. + if (null === $OAuth) { + return false; + } + $oauth = $OAuth->getOauth64(); + + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; + } + break; + default: + $this->setError("Authentication method \"$authtype\" is not supported"); + + return false; + } + + return true; + } + + /** + * Calculate an MD5 HMAC hash. + * Works like hash_hmac('md5', $data, $key) + * in case that function is not available. + * + * @param string $data The data to hash + * @param string $key The key to hash with + * + * @return string + */ + protected function hmac($data, $key) + { + if (function_exists('hash_hmac')) { + return hash_hmac('md5', $data, $key); + } + + // The following borrowed from + // http://php.net/manual/en/function.mhash.php#27225 + + // RFC 2104 HMAC implementation for php. + // Creates an md5 HMAC. + // Eliminates the need to install mhash to compute a HMAC + // by Lance Rushing + + $bytelen = 64; // byte length for md5 + if (strlen($key) > $bytelen) { + $key = pack('H*', md5($key)); + } + $key = str_pad($key, $bytelen, chr(0x00)); + $ipad = str_pad('', $bytelen, chr(0x36)); + $opad = str_pad('', $bytelen, chr(0x5c)); + $k_ipad = $key ^ $ipad; + $k_opad = $key ^ $opad; + + return md5($k_opad . pack('H*', md5($k_ipad . $data))); + } + + /** + * Check connection state. + * + * @return bool True if connected + */ + public function connected() + { + if (is_resource($this->smtp_conn)) { + $sock_status = stream_get_meta_data($this->smtp_conn); + if ($sock_status['eof']) { + // The socket is valid but we are not connected + $this->edebug( + 'SMTP NOTICE: EOF caught while checking if connected', + self::DEBUG_CLIENT + ); + $this->close(); + + return false; + } + + return true; // everything looks good + } + + return false; + } + + /** + * Close the socket and clean up the state of the class. + * Don't use this function without first trying to use QUIT. + * + * @see quit() + */ + public function close() + { + $this->setError(''); + $this->server_caps = null; + $this->helo_rply = null; + if (is_resource($this->smtp_conn)) { + // close the connection and cleanup + fclose($this->smtp_conn); + $this->smtp_conn = null; //Makes for cleaner serialization + $this->edebug('Connection: closed', self::DEBUG_CONNECTION); + } + } + + /** + * Send an SMTP DATA command. + * Issues a data command and sends the msg_data to the server, + * finializing the mail transaction. $msg_data is the message + * that is to be send with the headers. Each header needs to be + * on a single line followed by a with the message headers + * and the message body being separated by an additional . + * Implements RFC 821: DATA . + * + * @param string $msg_data Message data to send + * + * @return bool + */ + public function data($msg_data) + { + //This will use the standard timelimit + if (!$this->sendCommand('DATA', 'DATA', 354)) { + return false; + } + + /* The server is ready to accept data! + * According to rfc821 we should not send more than 1000 characters on a single line (including the LE) + * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into + * smaller lines to fit within the limit. + * We will also look for lines that start with a '.' and prepend an additional '.'. + * NOTE: this does not count towards line-length limit. + */ + + // Normalize line breaks before exploding + $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data)); + + /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field + * of the first line (':' separated) does not contain a space then it _should_ be a header and we will + * process all lines before a blank line as headers. + */ + + $field = substr($lines[0], 0, strpos($lines[0], ':')); + $in_headers = false; + if (!empty($field) and strpos($field, ' ') === false) { + $in_headers = true; + } + + foreach ($lines as $line) { + $lines_out = []; + if ($in_headers and $line == '') { + $in_headers = false; + } + //Break this line up into several smaller lines if it's too long + //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), + while (isset($line[self::MAX_LINE_LENGTH])) { + //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on + //so as to avoid breaking in the middle of a word + $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); + //Deliberately matches both false and 0 + if (!$pos) { + //No nice break found, add a hard break + $pos = self::MAX_LINE_LENGTH - 1; + $lines_out[] = substr($line, 0, $pos); + $line = substr($line, $pos); + } else { + //Break at the found point + $lines_out[] = substr($line, 0, $pos); + //Move along by the amount we dealt with + $line = substr($line, $pos + 1); + } + //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 + if ($in_headers) { + $line = "\t" . $line; + } + } + $lines_out[] = $line; + + //Send the lines to the server + foreach ($lines_out as $line_out) { + //RFC2821 section 4.5.2 + if (!empty($line_out) and $line_out[0] == '.') { + $line_out = '.' . $line_out; + } + $this->client_send($line_out . static::LE, 'DATA'); + } + } + + //Message data has been sent, complete the command + //Increase timelimit for end of DATA command + $savetimelimit = $this->Timelimit; + $this->Timelimit = $this->Timelimit * 2; + $result = $this->sendCommand('DATA END', '.', 250); + $this->recordLastTransactionID(); + //Restore timelimit + $this->Timelimit = $savetimelimit; + + return $result; + } + + /** + * Send an SMTP HELO or EHLO command. + * Used to identify the sending server to the receiving server. + * This makes sure that client and server are in a known state. + * Implements RFC 821: HELO + * and RFC 2821 EHLO. + * + * @param string $host The host name or IP to connect to + * + * @return bool + */ + public function hello($host = '') + { + //Try extended hello first (RFC 2821) + return $this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host); + } + + /** + * Send an SMTP HELO or EHLO command. + * Low-level implementation used by hello(). + * + * @param string $hello The HELO string + * @param string $host The hostname to say we are + * + * @return bool + * + * @see hello() + */ + protected function sendHello($hello, $host) + { + $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); + $this->helo_rply = $this->last_reply; + if ($noerror) { + $this->parseHelloFields($hello); + } else { + $this->server_caps = null; + } + + return $noerror; + } + + /** + * Parse a reply to HELO/EHLO command to discover server extensions. + * In case of HELO, the only parameter that can be discovered is a server name. + * + * @param string $type `HELO` or `EHLO` + */ + protected function parseHelloFields($type) + { + $this->server_caps = []; + $lines = explode("\n", $this->helo_rply); + + foreach ($lines as $n => $s) { + //First 4 chars contain response code followed by - or space + $s = trim(substr($s, 4)); + if (empty($s)) { + continue; + } + $fields = explode(' ', $s); + if (!empty($fields)) { + if (!$n) { + $name = $type; + $fields = $fields[0]; + } else { + $name = array_shift($fields); + switch ($name) { + case 'SIZE': + $fields = ($fields ? $fields[0] : 0); + break; + case 'AUTH': + if (!is_array($fields)) { + $fields = []; + } + break; + default: + $fields = true; + } + } + $this->server_caps[$name] = $fields; + } + } + } + + /** + * Send an SMTP MAIL command. + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. + * Implements RFC 821: MAIL FROM: . + * + * @param string $from Source address of this message + * + * @return bool + */ + public function mail($from) + { + $useVerp = ($this->do_verp ? ' XVERP' : ''); + + return $this->sendCommand( + 'MAIL FROM', + 'MAIL FROM:<' . $from . '>' . $useVerp, + 250 + ); + } + + /** + * Send an SMTP QUIT command. + * Closes the socket if there is no error or the $close_on_error argument is true. + * Implements from RFC 821: QUIT . + * + * @param bool $close_on_error Should the connection close if an error occurs? + * + * @return bool + */ + public function quit($close_on_error = true) + { + $noerror = $this->sendCommand('QUIT', 'QUIT', 221); + $err = $this->error; //Save any error + if ($noerror or $close_on_error) { + $this->close(); + $this->error = $err; //Restore any error from the quit command + } + + return $noerror; + } + + /** + * Send an SMTP RCPT command. + * Sets the TO argument to $toaddr. + * Returns true if the recipient was accepted false if it was rejected. + * Implements from RFC 821: RCPT TO: . + * + * @param string $address The address the message is being sent to + * + * @return bool + */ + public function recipient($address) + { + return $this->sendCommand( + 'RCPT TO', + 'RCPT TO:<' . $address . '>', + [250, 251] + ); + } + + /** + * Send an SMTP RSET command. + * Abort any transaction that is currently in progress. + * Implements RFC 821: RSET . + * + * @return bool True on success + */ + public function reset() + { + return $this->sendCommand('RSET', 'RSET', 250); + } + + /** + * Send a command to an SMTP server and check its return code. + * + * @param string $command The command name - not sent to the server + * @param string $commandstring The actual command to send + * @param int|array $expect One or more expected integer success codes + * + * @return bool True on success + */ + protected function sendCommand($command, $commandstring, $expect) + { + if (!$this->connected()) { + $this->setError("Called $command without being connected"); + + return false; + } + //Reject line breaks in all commands + if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) { + $this->setError("Command '$command' contained line breaks"); + + return false; + } + $this->client_send($commandstring . static::LE, $command); + + $this->last_reply = $this->get_lines(); + // Fetch SMTP code and possible error code explanation + $matches = []; + if (preg_match('/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/', $this->last_reply, $matches)) { + $code = $matches[1]; + $code_ex = (count($matches) > 2 ? $matches[2] : null); + // Cut off error code from each response line + $detail = preg_replace( + "/{$code}[ -]" . + ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', + '', + $this->last_reply + ); + } else { + // Fall back to simple parsing if regex fails + $code = substr($this->last_reply, 0, 3); + $code_ex = null; + $detail = substr($this->last_reply, 4); + } + + $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); + + if (!in_array($code, (array) $expect)) { + $this->setError( + "$command command failed", + $detail, + $code, + $code_ex + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, + self::DEBUG_CLIENT + ); + + return false; + } + + $this->setError(''); + + return true; + } + + /** + * Send an SMTP SAML command. + * Starts a mail transaction from the email address specified in $from. + * Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. This command + * will send the message to the users terminal if they are logged + * in and send them an email. + * Implements RFC 821: SAML FROM: . + * + * @param string $from The address the message is from + * + * @return bool + */ + public function sendAndMail($from) + { + return $this->sendCommand('SAML', "SAML FROM:$from", 250); + } + + /** + * Send an SMTP VRFY command. + * + * @param string $name The name to verify + * + * @return bool + */ + public function verify($name) + { + return $this->sendCommand('VRFY', "VRFY $name", [250, 251]); + } + + /** + * Send an SMTP NOOP command. + * Used to keep keep-alives alive, doesn't actually do anything. + * + * @return bool + */ + public function noop() + { + return $this->sendCommand('NOOP', 'NOOP', 250); + } + + /** + * Send an SMTP TURN command. + * This is an optional command for SMTP that this class does not support. + * This method is here to make the RFC821 Definition complete for this class + * and _may_ be implemented in future. + * Implements from RFC 821: TURN . + * + * @return bool + */ + public function turn() + { + $this->setError('The SMTP TURN command is not implemented'); + $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); + + return false; + } + + /** + * Send raw data to the server. + * + * @param string $data The data to send + * @param string $command Optionally, the command this is part of, used only for controlling debug output + * + * @return int|bool The number of bytes sent to the server or false on error + */ + public function client_send($data, $command = '') + { + //If SMTP transcripts are left enabled, or debug output is posted online + //it can leak credentials, so hide credentials in all but lowest level + if (self::DEBUG_LOWLEVEL > $this->do_debug and + in_array($command, ['User & Password', 'Username', 'Password'], true)) { + $this->edebug('CLIENT -> SERVER:
+
+
+ + + + + +
+
+ + + +

+ +

+ + +
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ tabindex="5" /> + +
+ + +
+
+
@@ -763,8 +708,8 @@ case 5:
  • -
  • -
  • +
  • +
  • diff --git a/app/views/helpers/category/update.phtml b/app/views/helpers/category/update.phtml index a2ee3e2ef..b64bd786a 100644 --- a/app/views/helpers/category/update.phtml +++ b/app/views/helpers/category/update.phtml @@ -11,7 +11,10 @@
    - + category->id() == FreshRSS_CategoryDAO::DEFAULTCATEGORYID ? 'disabled="disabled"' : ''; + ?> />
    diff --git a/cli/do-install.php b/cli/do-install.php index fd5aa4a3c..dea5d235e 100755 --- a/cli/do-install.php +++ b/cli/do-install.php @@ -3,7 +3,7 @@ require(__DIR__ . '/_cli.php'); if (!file_exists(DATA_PATH . '/do-install.txt')) { - fail('FreshRSS looks to be already installed! Please use `./cli/reconfigure.php` instead.'); + fail('FreshRSS seems to be already installed! Please use `./cli/reconfigure.php` instead.'); } $params = array( @@ -82,10 +82,9 @@ if (file_put_contents(join_path(DATA_PATH, 'config.php'), fail('FreshRSS could not write configuration file!: ' . join_path(DATA_PATH, 'config.php')); } -$config['db']['default_user'] = $config['default_user']; -if (!checkDb($config['db'])) { +if (!checkDb()) { @unlink(join_path(DATA_PATH, 'config.php')); - fail('FreshRSS database error: ' . (empty($config['db']['error']) ? 'Unknown error' : $config['db']['error'])); + fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error'])); } echo '• Remember to create the default user: ', $config['default_user'] , "\n", diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index 4d5e47da9..3fabb73c8 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -6,45 +6,35 @@ /** * La classe Model_sql représente le modèle interragissant avec les bases de données - * Seul la connexion MySQL est prise en charge pour le moment */ class Minz_ModelPdo { /** * Partage la connexion à la base de données entre toutes les instances. */ - public static $useSharedBd = true; - private static $sharedBd = null; + public static $usesSharedPdo = true; + private static $sharedPdo = null; private static $sharedPrefix; private static $sharedCurrentUser; - /** - * $bd variable représentant la base de données - */ - protected $bd; - + protected $pdo; protected $current_user; - protected $prefix; /** * Créé la connexion à la base de données à l'aide des variables * HOST, BASE, USER et PASS définies dans le fichier de configuration */ - public function __construct($currentUser = null, $currentPrefix = null, $currentDb = null) { + public function __construct($currentUser = null, $currentPdo = null) { if ($currentUser === null) { $currentUser = Minz_Session::param('currentUser'); } - if ($currentPrefix !== null) { - $this->prefix = $currentPrefix; - } - if ($currentDb != null) { - $this->bd = $currentDb; + if ($currentPdo != null) { + $this->pdo = $currentPdo; return; } - if (self::$useSharedBd && self::$sharedBd != null && - ($currentUser == null || $currentUser === self::$sharedCurrentUser)) { - $this->bd = self::$sharedBd; - $this->prefix = self::$sharedPrefix; + if (self::$usesSharedPdo && self::$sharedPdo != null && + ($currentUser == '' || $currentUser === self::$sharedCurrentUser)) { + $this->pdo = self::$sharedPdo; $this->current_user = self::$sharedCurrentUser; return; } @@ -54,35 +44,39 @@ class Minz_ModelPdo { $conf = Minz_Configuration::get('system'); $db = $conf->db; - $driver_options = isset($conf->db['pdo_options']) && is_array($conf->db['pdo_options']) ? $conf->db['pdo_options'] : array(); + $driver_options = isset($db['pdo_options']) && is_array($db['pdo_options']) ? $db['pdo_options'] : []; $dbServer = parse_url('db://' . $db['host']); + $dsn = ''; try { switch ($db['type']) { case 'mysql': - $string = 'mysql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']) . ';dbname=' . $db['base'] . ';charset=utf8mb4'; + $dsn = 'mysql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']) . ';charset=utf8mb4'; + if (!empty($db['base'])) { + $dsn .= ';dbname=' . $db['base']; + } if (!empty($dbServer['port'])) { - $string .= ';port=' . $dbServer['port']; + $dsn .= ';port=' . $dbServer['port']; } $driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4'; - $this->prefix = $db['prefix'] . $currentUser . '_'; - $this->bd = new MinzPDOMySql($string, $db['user'], $db['password'], $driver_options); - $this->bd->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + $this->pdo = new MinzPDOMySql($dsn, $db['user'], $db['password'], $driver_options); + $this->pdo->setPrefix($db['prefix'] . $currentUser . '_'); break; case 'sqlite': - $string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite'); - $this->prefix = ''; - $this->bd = new MinzPDOSQLite($string, $db['user'], $db['password'], $driver_options); - $this->bd->exec('PRAGMA foreign_keys = ON;'); + $dsn = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite'); + $this->pdo = new MinzPDOSQLite($dsn, $db['user'], $db['password'], $driver_options); + $this->pdo->setPrefix(''); break; case 'pgsql': - $string = 'pgsql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']) . ';dbname=' . $db['base']; + $dsn = 'pgsql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']); + if (!empty($db['base'])) { + $dsn .= ';dbname=' . $db['base']; + } if (!empty($dbServer['port'])) { - $string .= ';port=' . $dbServer['port']; + $dsn .= ';port=' . $dbServer['port']; } - $this->prefix = $db['prefix'] . $currentUser . '_'; - $this->bd = new MinzPDOPGSQL($string, $db['user'], $db['password'], $driver_options); - $this->bd->exec("SET NAMES 'UTF8';"); + $this->pdo = new MinzPDOPGSQL($dsn, $db['user'], $db['password'], $driver_options); + $this->pdo->setPrefix($db['prefix'] . $currentUser . '_'); break; default: throw new Minz_PDOConnectionException( @@ -91,69 +85,86 @@ class Minz_ModelPdo { ); break; } - self::$sharedBd = $this->bd; - self::$sharedPrefix = $this->prefix; + self::$sharedPdo = $this->pdo; } catch (Exception $e) { throw new Minz_PDOConnectionException( - $string, + $dsn, $db['user'], Minz_Exception::ERROR ); } } public function beginTransaction() { - $this->bd->beginTransaction(); + $this->pdo->beginTransaction(); } public function inTransaction() { - return $this->bd->inTransaction(); + return $this->pdo->inTransaction(); } public function commit() { - $this->bd->commit(); + $this->pdo->commit(); } public function rollBack() { - $this->bd->rollBack(); + $this->pdo->rollBack(); } public static function clean() { - self::$sharedBd = null; + self::$sharedPdo = null; self::$sharedCurrentUser = ''; - self::$sharedPrefix = ''; } } abstract class MinzPDO extends PDO { - private static function check($statement) { + public function __construct($dsn, $username = null, $passwd = null, $options = null) { + parent::__construct($dsn, $username, $passwd, $options); + $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + } + + abstract public function dbType(); + + private $prefix = ''; + public function prefix() { return $this->prefix; } + public function setPrefix($prefix) { $this->prefix = $prefix; } + + private function autoPrefix($sql) { + return str_replace('`_', '`' . $this->prefix, $sql); + } + + protected function preSql($statement) { if (preg_match('/^(?:UPDATE|INSERT|DELETE)/i', $statement)) { invalidateHttpCache(); } + return $this->autoPrefix($statement); } - protected function compatibility($statement) { - return $statement; + public function lastInsertId($name = null) { + if ($name != null) { + $name = $this->preSql($name); + } + return parent::lastInsertId($name); } - abstract public function dbType(); - public function prepare($statement, $driver_options = array()) { - MinzPDO::check($statement); - $statement = $this->compatibility($statement); + $statement = $this->preSql($statement); return parent::prepare($statement, $driver_options); } public function exec($statement) { - MinzPDO::check($statement); - $statement = $this->compatibility($statement); + $statement = $this->preSql($statement); return parent::exec($statement); } public function query($statement) { - MinzPDO::check($statement); - $statement = $this->compatibility($statement); + $statement = $this->preSql($statement); return parent::query($statement); } } class MinzPDOMySql extends MinzPDO { + public function __construct($dsn, $username = null, $passwd = null, $options = null) { + parent::__construct($dsn, $username, $passwd, $options); + $this->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + } + public function dbType() { return 'mysql'; } @@ -164,6 +175,11 @@ class MinzPDOMySql extends MinzPDO { } class MinzPDOSQLite extends MinzPDO { + public function __construct($dsn, $username = null, $passwd = null, $options = null) { + parent::__construct($dsn, $username, $passwd, $options); + $this->exec('PRAGMA foreign_keys = ON;'); + } + public function dbType() { return 'sqlite'; } @@ -174,11 +190,17 @@ class MinzPDOSQLite extends MinzPDO { } class MinzPDOPGSQL extends MinzPDO { + public function __construct($dsn, $username = null, $passwd = null, $options = null) { + parent::__construct($dsn, $username, $passwd, $options); + $this->exec("SET NAMES 'UTF8';"); + } + public function dbType() { return 'pgsql'; } - protected function compatibility($statement) { + protected function preSql($statement) { + $statement = parent::preSql($statement); return str_replace(array('`', ' LIKE '), array('"', ' ILIKE '), $statement); } } diff --git a/lib/favicons.php b/lib/favicons.php index 7a2d1187e..bc82b57b9 100644 --- a/lib/favicons.php +++ b/lib/favicons.php @@ -1,6 +1,6 @@ true, CURLOPT_TIMEOUT => 15, CURLOPT_USERAGENT => FRESHRSS_USERAGENT, CURLOPT_MAXREDIRS => 10, - )); - if (version_compare(PHP_VERSION, '5.6.0') >= 0 || ini_get('open_basedir') == '') { - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); //Keep option separated for open_basedir PHP bug 65646 - } - if (defined('CURLOPT_ENCODING')) { - curl_setopt($ch, CURLOPT_ENCODING, ''); //Enable all encodings - } + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_ENCODING => '', //Enable all encodings + ]); curl_setopt_array($ch, $curlOptions); $response = curl_exec($ch); $info = curl_getinfo($ch); @@ -89,7 +85,6 @@ function searchFavicon(&$url) { } function download_favicon($url, $dest) { - global $default_favicon; $url = trim($url); $favicon = searchFavicon($url); if ($favicon == '') { @@ -109,5 +104,5 @@ function download_favicon($url, $dest) { } } return ($favicon != '' && file_put_contents($dest, $favicon)) || - @copy($default_favicon, $dest); + @copy(DEFAULT_FAVICON, $dest); } diff --git a/lib/lib_install.php b/lib/lib_install.php index 17defccf6..ed361eb39 100644 --- a/lib/lib_install.php +++ b/lib/lib_install.php @@ -78,69 +78,24 @@ function generateSalt() { return sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__))); } -function checkDb(&$dbOptions) { - $dsn = ''; - $driver_options = null; - prepareSyslog(); - try { - switch ($dbOptions['type']) { - case 'mysql': - include_once(APP_PATH . '/SQL/install.sql.mysql.php'); - $driver_options = array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4' - ); - try { // on ouvre une connexion juste pour créer la base si elle n'existe pas - $dsn = 'mysql:host=' . $dbOptions['host'] . ';'; - $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options); - $sql = sprintf(SQL_CREATE_DB, $dbOptions['base']); - $res = $c->query($sql); - } catch (PDOException $e) { - syslog(LOG_DEBUG, 'FreshRSS MySQL warning: ' . $e->getMessage()); - } - // on écrase la précédente connexion en sélectionnant la nouvelle BDD - $dsn = 'mysql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['base']; - break; - case 'sqlite': - include_once(APP_PATH . '/SQL/install.sql.sqlite.php'); - $path = join_path(USERS_PATH, $dbOptions['default_user']); - if (!is_dir($path)) { - mkdir($path); - } - $dsn = 'sqlite:' . join_path($path, 'db.sqlite'); - $driver_options = array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ); - break; - case 'pgsql': - include_once(APP_PATH . '/SQL/install.sql.pgsql.php'); - $driver_options = array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ); - try { // on ouvre une connexion juste pour créer la base si elle n'existe pas - $dsn = 'pgsql:host=' . $dbOptions['host'] . ';dbname=postgres'; - $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options); - $sql = sprintf(SQL_CREATE_DB, $dbOptions['base']); - $res = $c->query($sql); - } catch (PDOException $e) { - syslog(LOG_DEBUG, 'FreshRSS PostgreSQL warning: ' . $e->getMessage()); - } - // on écrase la précédente connexion en sélectionnant la nouvelle BDD - $dsn = 'pgsql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['base']; - break; - default: - return false; - } - - $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options); - $res = $c->query('SELECT 1'); - } catch (PDOException $e) { - $dsn = ''; - syslog(LOG_DEBUG, 'FreshRSS SQL warning: ' . $e->getMessage()); - $dbOptions['error'] = $e->getMessage(); +function checkDb() { + $conf = FreshRSS_Context::$system_conf; + $db = $conf->db; + if (empty($db['pdo_options'])) { + $db['pdo_options'] = []; } - $dbOptions['dsn'] = $dsn; - $dbOptions['options'] = $driver_options; - return $dsn != ''; + $db['pdo_options'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; + $dbBase = isset($db['base']) ? $db['base'] : ''; + + $db['base'] = ''; //First connection without database name to create the database + Minz_ModelPdo::$usesSharedPdo = false; + $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + $databaseDAO->create(); + + $db['base'] = $dbBase; //New connection with the database name + $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + Minz_ModelPdo::$usesSharedPdo = true; + return $databaseDAO->testConnection(); } function deleteInstall() { diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 1bba60c36..854126b54 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -535,17 +535,16 @@ function _i($icon, $url_only = false) { } -$SHORTCUT_KEYS = array( //No const for < PHP 5.6 compatibility +const SHORTCUT_KEYS = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Backspace', 'Delete', 'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab', - ); + ]; function validateShortcutList($shortcuts) { - global $SHORTCUT_KEYS; $legacy = array( 'down' => 'ArrowDown', 'left' => 'ArrowLeft', 'page_down' => 'PageDown', 'page_up' => 'PageUp', 'right' => 'ArrowRight', 'up' => 'ArrowUp', @@ -554,17 +553,17 @@ function validateShortcutList($shortcuts) { $shortcuts_ok = array(); foreach ($shortcuts as $key => $value) { - if (in_array($value, $SHORTCUT_KEYS)) { + if (in_array($value, SHORTCUT_KEYS)) { $shortcuts_ok[$key] = $value; } elseif (isset($legacy[$value])) { $shortcuts_ok[$key] = $legacy[$value]; } else { //Case-insensitive search if ($upper === null) { - $upper = array_map('strtoupper', $SHORTCUT_KEYS); + $upper = array_map('strtoupper', SHORTCUT_KEYS); } $i = array_search(strtoupper($value), $upper); if ($i !== false) { - $shortcuts_ok[$key] = $SHORTCUT_KEYS[$i]; + $shortcuts_ok[$key] = SHORTCUT_KEYS[$i]; } } } diff --git a/p/api/fever.php b/p/api/fever.php index b81646928..30b85dafd 100644 --- a/p/api/fever.php +++ b/p/api/fever.php @@ -95,7 +95,7 @@ class FeverDAO extends Minz_ModelPdo $sql = 'SELECT id, guid, title, author, ' . ($entryDAO->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') . ', link, date, is_read, is_favorite, id_feed ' - . 'FROM `' . $this->prefix . 'entry` WHERE'; + . 'FROM `_entry` WHERE'; if (!empty($entry_ids)) { $bindEntryIds = $this->bindParamArray('id', $entry_ids, $values); @@ -120,7 +120,7 @@ class FeverDAO extends Minz_ModelPdo $sql .= $order; $sql .= ' LIMIT 50'; - $stm = $this->bd->prepare($sql); + $stm = $this->pdo->prepare($sql); $stm->execute($values); $result = $stm->fetchAll(PDO::FETCH_ASSOC); diff --git a/p/api/greader.php b/p/api/greader.php index b6777796a..77e498524 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -76,12 +76,6 @@ function multiplePosts($name) { //https://bugs.php.net/bug.php?id=51633 return $result; } -class MyPDO extends Minz_ModelPdo { - function prepare($sql) { - return $this->bd->prepare(str_replace('%_', $this->prefix, $sql)); - } -} - function debugInfo() { if (function_exists('getallheaders')) { $ALL_HEADERS = getallheaders(); @@ -239,9 +233,8 @@ function userInfo() { //https://github.com/theoldreader/api#user-info function tagList() { header('Content-Type: application/json; charset=UTF-8'); - $pdo = new MyPDO(); - $stm = $pdo->prepare('SELECT c.name FROM `%_category` c'); - $stm->execute(); + $model = new Minz_ModelPdo(); + $stm = $model->pdo->query('SELECT c.name FROM `_category` c'); $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); $tags = array( @@ -277,10 +270,11 @@ function tagList() { function subscriptionList() { header('Content-Type: application/json; charset=UTF-8'); - $pdo = new MyPDO(); - $stm = $pdo->prepare('SELECT f.id, f.name, f.url, f.website, c.id as c_id, c.name as c_name FROM `%_feed` f - INNER JOIN `%_category` c ON c.id = f.category AND f.priority >= :priority_normal'); - $stm->execute(array(':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL)); + $model = new Minz_ModelPdo(); + $stm = $model->pdo->prepare('SELECT f.id, f.name, f.url, f.website, c.id as c_id, c.name as c_name FROM `_feed` f + INNER JOIN `_category` c ON c.id = f.category AND f.priority >= :priority_normal'); + $stm->bindValue(':priority_normal', FreshRSS_Feed::PRIORITY_NORMAL, PDO::PARAM_INT); + $stm->execute(); $res = $stm->fetchAll(PDO::FETCH_ASSOC); $salt = FreshRSS_Context::$system_conf->salt; diff --git a/p/f.php b/p/f.php index b68109cd5..9947539c3 100644 --- a/p/f.php +++ b/p/f.php @@ -5,13 +5,11 @@ require(LIB_PATH . '/favicons.php'); require(LIB_PATH . '/http-conditional.php'); function show_default_favicon($cacheSeconds = 3600) { - global $default_favicon; - header('Content-Disposition: inline; filename="default_favicon.ico"'); - $default_mtime = @filemtime($default_favicon); + $default_mtime = @filemtime(DEFAULT_FAVICON); if (!httpConditional($default_mtime, $cacheSeconds, 2)) { - readfile($default_favicon); + readfile(DEFAULT_FAVICON); } } @@ -20,8 +18,8 @@ if (!ctype_xdigit($id)) { $id = '0'; } -$txt = $favicons_dir . $id . '.txt'; -$ico = $favicons_dir . $id . '.ico'; +$txt = FAVICONS_DIR . $id . '.txt'; +$ico = FAVICONS_DIR . $id . '.ico'; $ico_mtime = @filemtime($ico); $txt_mtime = @filemtime($txt); -- cgit v1.2.3 From 37b52b7361d3ac15273ca19a0b96ef74299e759e Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 1 Oct 2019 18:12:21 +0200 Subject: Trim whitespace (#2544) --- CHANGELOG.md | 4 +- Docker/README.md | 2 +- app/shares.php | 10 +- app/views/helpers/index/normal/entry_header.phtml | 29 +++--- app/views/index/tos.phtml | 20 ++-- app/views/user/profile.phtml | 6 +- app/views/user/validateEmail.phtml | 34 +++---- cli/README.md | 2 +- docs/en/admins/02_Installation.md | 2 +- docs/en/admins/03_Updating.md | 4 +- docs/en/contributing.md | 2 +- docs/en/developers/01_First_steps.md | 42 ++++----- docs/en/developers/03_Backend/05_Extensions.md | 110 +++++++++++----------- docs/en/users/03_Main_view.md | 16 ++-- docs/en/users/05_Configuration.md | 2 +- docs/en/users/07_Frequently_Asked_Questions.md | 2 +- docs/fr/developers/01_First_steps.md | 40 ++++---- docs/fr/developers/03_Backend/05_Extensions.md | 78 +++++++-------- docs/fr/users/06_Mobile_access.md | 4 +- docs/fr/users/07_Frequently_Asked_Questions.md | 4 +- lib/Minz/ModelPdo.php | 6 +- 21 files changed, 209 insertions(+), 210 deletions(-) (limited to 'cli') diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa49aafe..e68c9a2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -522,7 +522,7 @@ * Simplified Chinese [#1541](https://github.com/FreshRSS/FreshRSS/pull/1541) * Improve English [#1465](https://github.com/FreshRSS/FreshRSS/pull/1465) * Improve Dutch [#1559](https://github.com/FreshRSS/FreshRSS/pull/1559) - * Added Spanish language [#1631] (https://github.com/FreshRSS/FreshRSS/pull/1631/) + * Added Spanish language [#1631] (https://github.com/FreshRSS/FreshRSS/pull/1631/) * Security * Do not require write access to check availability of new versions [#1450](https://github.com/FreshRSS/FreshRSS/issues/1450) * Misc. @@ -548,7 +548,7 @@ * New command `./cli/reconfigure.php` to update an existing installation [#1439](https://github.com/FreshRSS/FreshRSS/pull/1439) * Many CLI improvements [#1447](https://github.com/FreshRSS/FreshRSS/pull/1447) * More information (number of feeds, articles, etc.) in `./cli/user-info.php` - * Better idempotency of `./cli/do-install.php` and language parameter [#1449](https://github.com/FreshRSS/FreshRSS/issues/1449) + * Better idempotency of `./cli/do-install.php` and language parameter [#1449](https://github.com/FreshRSS/FreshRSS/issues/1449) * Bug fixing * Fix several CLI issues [#1445](https://github.com/FreshRSS/FreshRSS/issues/1445) * Fix CLI install bugs with SQLite [#1443](https://github.com/FreshRSS/FreshRSS/issues/1443), [#1448](https://github.com/FreshRSS/FreshRSS/issues/1448) diff --git a/Docker/README.md b/Docker/README.md index 4146a57dd..13988a316 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -48,7 +48,7 @@ docker run -d --restart unless-stopped --log-opt max-size=10m \ See [more information about Docker and Let’s Encrypt in Træfik](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/). -## Run FreshRSS +## Run FreshRSS Example using the built-in refresh cron job (see further below for alternatives). You must first chose a domain (DNS) or sub-domain, e.g. `freshrss.example.net`. diff --git a/app/shares.php b/app/shares.php index 71860c9e3..9df83617a 100644 --- a/app/shares.php +++ b/app/shares.php @@ -139,9 +139,9 @@ return array( 'method' => 'GET', ), 'lemmy' => array( - 'url' => '~URL~/create_post?url=~LINK~&name=~TITLE~', - 'transform' => array('rawurlencode'), - 'form' => 'advanced', - 'method' => 'GET', - ), + 'url' => '~URL~/create_post?url=~LINK~&name=~TITLE~', + 'transform' => array('rawurlencode'), + 'form' => 'advanced', + 'method' => 'GET', + ), ); diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml index 7873b16e4..82c209bb2 100644 --- a/app/views/helpers/index/normal/entry_header.phtml +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -28,21 +28,20 @@ } } ?>
  • ✇ feed->name(); ?>
  • -
  • entry->title(); ?>
    - entry->authors(); - if (is_array($authors)): - $first = true; - foreach ($authors as $author): - echo $first ? $author : ', ' . $author; - $first = false; - endforeach; - endif; - ?>
  • +
  • entry->title(); ?>
    entry->authors(); + if (is_array($authors)) { + $first = true; + foreach ($authors as $author) { + echo $first ? $author : ', ' . $author; + $first = false; + } + } + ?>
  • entry->date(); ?> 
  • diff --git a/app/views/index/tos.phtml b/app/views/index/tos.phtml index 79c597244..1b3498134 100644 --- a/app/views/index/tos.phtml +++ b/app/views/index/tos.phtml @@ -1,13 +1,13 @@
    - can_register) { ?> - - - - - - - - + can_register) { ?> + + + + + + + + - terms_of_service; ?> + terms_of_service; ?>
    diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml index df43642dd..de717b36e 100644 --- a/app/views/user/profile.phtml +++ b/app/views/user/profile.phtml @@ -1,7 +1,7 @@ disable_aside) { - $this->partial('aside_configure'); - } + if (!$this->disable_aside) { + $this->partial('aside_configure'); + } ?>
    diff --git a/app/views/user/validateEmail.phtml b/app/views/user/validateEmail.phtml index a246c222e..51517f5eb 100644 --- a/app/views/user/validateEmail.phtml +++ b/app/views/user/validateEmail.phtml @@ -1,22 +1,22 @@
    -

    - title); ?> -

    +

    + title); ?> +

    -

    - mail_login); ?> -

    +

    + mail_login); ?> +

    - - - - +
    + + +
    -

    - - - -

    +

    + + + +

    diff --git a/cli/README.md b/cli/README.md index 35c9bad9b..89b440a39 100644 --- a/cli/README.md +++ b/cli/README.md @@ -128,4 +128,4 @@ Example to get the number of feeds of a given user: # Install and updates If you want to administrate FreshRSS using git, please read our [installation docs](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html) -and [update guidelines](https://freshrss.github.io/FreshRSS/en/admins/03_Updating.html). +and [update guidelines](https://freshrss.github.io/FreshRSS/en/admins/03_Updating.html). diff --git a/docs/en/admins/02_Installation.md b/docs/en/admins/02_Installation.md index 446ef0dcf..7bba647ec 100644 --- a/docs/en/admins/02_Installation.md +++ b/docs/en/admins/02_Installation.md @@ -137,7 +137,7 @@ A step-by-step tutorial is available [in French](http://www.pihomeserver.fr/2013 # Security -Make sure to expose only the `./p/` folder on the web, the other directories contain personal and sensitive data. +Make sure to expose only the `./p/` folder on the web, the other directories contain personal and sensitive data. See the Apache and nginx config examples above. **TODO** diff --git a/docs/en/admins/03_Updating.md b/docs/en/admins/03_Updating.md index 4e1fdfa5d..461366049 100644 --- a/docs/en/admins/03_Updating.md +++ b/docs/en/admins/03_Updating.md @@ -15,7 +15,7 @@ The update process depends on your installation type, see below: Change to your installation at http://localhost/FreshRSS/p/i/?c=update and hit the "Check for new updates" button. -If there is a new version you will be prompted again. +If there is a new version you will be prompted again. ## Using git @@ -72,7 +72,7 @@ cd /usr/share/FreshRSS ``` Commands intended to be executed in order (you can c/p the whole block if desired): - + ```sh wget https://github.com/FreshRSS/FreshRSS/archive/master.zip unzip master.zip diff --git a/docs/en/contributing.md b/docs/en/contributing.md index 45c1650fb..870e0c14f 100644 --- a/docs/en/contributing.md +++ b/docs/en/contributing.md @@ -52,5 +52,5 @@ We are working on a better way to handle internationalization but don't hesitate ## Contribute to documentation -The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. +The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. If you want to give some help, meet us in the main repositories [docs directory](https://github.com/FreshRSS/FreshRSS/tree/master/docs)! diff --git a/docs/en/developers/01_First_steps.md b/docs/en/developers/01_First_steps.md index 6b7f437a7..28c249be4 100644 --- a/docs/en/developers/01_First_steps.md +++ b/docs/en/developers/01_First_steps.md @@ -61,7 +61,7 @@ The `TAG` variable can be anything (e.g. `dev-local`). You can target a specific # Extensions -If you want to create your own FreshRSS extension, take a look at the [extension documentation](03_Backend/05_Extensions.md). +If you want to create your own FreshRSS extension, take a look at the [extension documentation](03_Backend/05_Extensions.md). # Coding style @@ -110,7 +110,7 @@ There is a space before and after every operator. ```php if ($a == 10) { - // do something + // do something } echo $a ? 1 : 0; @@ -122,11 +122,11 @@ There is no spaces in the brackets. There is no space before the opening bracket ```php if ($a == 10) { - // do something + // do something } if ((int)$a == 10) { - // do something + // do something } ``` @@ -137,16 +137,16 @@ It happens most of the time in Javascript files. When there is chained functions ```javascript // First instruction shortcut.add(shortcuts.mark_read, function () { - //... - }, { - 'disable_in_input': true - }); + //... + }, { + 'disable_in_input': true + }); // Second instruction shortcut.add("shift+" + shortcuts.mark_read, function () { - //... - }, { - 'disable_in_input': true - }); + //... + }, { + 'disable_in_input': true + }); ``` ## Line length @@ -158,7 +158,7 @@ With functions, parameters can be declared on different lines. ```php function my_function($param_1, $param_2, $param_3, $param_4) { - // do something + // do something } ``` @@ -173,7 +173,7 @@ They must follow the "snake case" convention. ```php // a function function function_name() { - // do something + // do something } // a variable $variable_name; @@ -185,7 +185,7 @@ They must follow the "lower camel case" convention. ```php private function methodName() { - // do something + // do something } ``` @@ -213,7 +213,7 @@ They must be at the end of the line if a condition runs on more than one line. ```php if ($a == 10 || $a == 20) { - // do something + // do something } ``` @@ -226,9 +226,9 @@ If the file contains only PHP code, the PHP closing tag must be omitted. If an array declaration runs on more than one line, each element must be followed by a comma even the last one. ```php -$variable = array( - "value 1", - "value 2", - "value 3", -); +$variable = [ + "value 1", + "value 2", + "value 3", +]; ``` diff --git a/docs/en/developers/03_Backend/05_Extensions.md b/docs/en/developers/03_Backend/05_Extensions.md index 7c1f8c046..0cfa5c8b7 100644 --- a/docs/en/developers/03_Backend/05_Extensions.md +++ b/docs/en/developers/03_Backend/05_Extensions.md @@ -48,13 +48,13 @@ Code example: view->a_variable = 'FooBar'; - } + public function indexAction() { + $this->view->a_variable = 'FooBar'; + } - public function worldAction() { - $this->view->a_variable = 'Hello World!'; - } + public function worldAction() { + $this->view->a_variable = 'Hello World!'; + } } ?> @@ -74,7 +74,7 @@ As explained above, the views consist of HTML mixed with PHP. Code example: ```html

    - This is a parameter passed from the controller: a_variable; ?> + This is a parameter passed from the controller: a_variable; ?>

    ``` @@ -119,7 +119,7 @@ To take full advantage of the Minz routing system, it is strongly discouraged to ```html

    - Go to page Hello world! + Go to page Hello world!

    ``` @@ -130,13 +130,13 @@ So use the `Minz_Url` class and its `display()` method instead. `Minz_Url::displ ```php 'hello', - 'a' => 'world', - 'params' => array( - 'foo' => 'bar', - ) -); +$url_array = [ + 'c' => 'hello', + 'a' => 'world', + 'params' => [ + 'foo' => 'bar', + ], +]; // Show something like .?c=hello&a=world&foo=bar echo Minz_Url::display($url_array); @@ -166,10 +166,10 @@ Code example: ```php 'hello', - 'a' => 'world' -); +$url_array = [ + 'c' => 'hello', + 'a' => 'world', +]; // Tells Minz to redirect the user to the hello / world page. // Note that this is a redirection in the Minz sense of the term, not a redirection that the browser will have to manage (HTTP code 301 or 302) @@ -188,10 +188,10 @@ It is very common to want display a message to the user while performing a redir ```php 'hello', - 'a' => 'world' -); +$url_array = [ + 'c' => 'hello', + 'a' => 'world', +]; $feedback_good = 'Tout s\'est bien passé !'; $feedback_bad = 'Oups, quelque chose n\'a pas marché.'; @@ -226,18 +226,18 @@ The translation files are quite simple: it is only a matter of returning a PHP t array( - 'actualize' => 'Actualiser', - 'back_to_rss_feeds' => '← Retour à vos flux RSS', - 'cancel' => 'Annuler', - 'create' => 'Créer', - 'disable' => 'Désactiver', - ), - 'freshrss' => array( - '_' => 'FreshRSS', - 'about' => 'À propos de FreshRSS', - ), -); + 'action' => [ + 'actualize' => 'Actualiser', + 'back_to_rss_feeds' => '← Retour à vos flux RSS', + 'cancel' => 'Annuler', + 'create' => 'Créer', + 'disable' => 'Désactiver', + ), + 'freshrss' => array( + '_' => 'FreshRSS', + 'about' => 'À propos de FreshRSS', + ), +]; ?> ``` @@ -247,9 +247,9 @@ Code example: ```html

    - - - + + +

    ``` @@ -267,8 +267,8 @@ An extension allows you to add functionality easily to FreshRSS without having t ### Basic files and folders -The first thing to note is that **all** extensions **must** be located in the `extensions` directory, at the base of the FreshRSS tree. -An extension is a directory containing a set of mandatory (and optional) files and subdirectories. +The first thing to note is that **all** extensions **must** be located in the `extensions` directory, at the base of the FreshRSS tree. +An extension is a directory containing a set of mandatory (and optional) files and subdirectories. The convention requires that the main directory name be preceded by an "x" to indicate that it is not an extension included by default in FreshRSS. The main directory of an extension must contain at least two **mandatory** files: @@ -276,16 +276,16 @@ The main directory of an extension must contain at least two **mandatory** files - A `metadata.json` file that contains a description of the extension. This file is written in JSON. - An `extension.php` file containing the entry point of the extension (which is a class that inherits Minz_Extension). -Please note that there is a not a required link between the directory name of the extension and the name of the class inside `extension.php`, -but you should follow our best practice: +Please note that there is a not a required link between the directory name of the extension and the name of the class inside `extension.php`, +but you should follow our best practice: If you want to write a `HelloWorld` extension, the directory name should be `xExtension-HelloWorld` and the base class name `HelloWorldExtension`. In the file `freshrss/extensions/xExtension-HelloWorld/extension.php` you need the structure: ```html class HelloWorldExtension extends Minz_Extension { - public function init() { - // your code here - } + public function init() { + // your code here + } } ``` There is an example HelloWorld extension that you can download from [our GitHub repo](https://github.com/FreshRSS/xExtension-HelloWorld). @@ -315,14 +315,14 @@ Only the `name` and` entrypoint` fields are required. ### Choose between « system » or « user » -A __user__ extension can be enabled by some users and not by others (typically for user preferences). +A __user__ extension can be enabled by some users and not by others (typically for user preferences). A __system__ extension in comparison is enabled for every account. ### Writing your own extension.php -This file is the entry point of your extension. It must contain a specific class to function. -As mentioned above, the name of the class must be your `entrypoint` suffixed by` Extension` (`HelloWorldExtension` for example). +This file is the entry point of your extension. It must contain a specific class to function. +As mentioned above, the name of the class must be your `entrypoint` suffixed by` Extension` (`HelloWorldExtension` for example). In addition, this class must be inherited from the `Minz_Extension` class to benefit from extensions-specific methods. Your class will benefit from four methods to redefine: @@ -351,13 +351,13 @@ You can register at the FreshRSS event system in an extensions `init()` method, ```html class HelloWorldExtension extends Minz_Extension { - public function init() { - $this->registerHook('entry_before_display', array($this, 'renderEntry')); - } - public function renderEntry($entry) { - $entry->_content('

    Hello World

    ' . $entry->content()); - return $entry; - } + public function init() { + $this->registerHook('entry_before_display', array($this, 'renderEntry')); + } + public function renderEntry($entry) { + $entry->_content('

    Hello World

    ' . $entry->content()); + return $entry; + } } ``` diff --git a/docs/en/users/03_Main_view.md b/docs/en/users/03_Main_view.md index 59d051e7e..c6c3e3b50 100644 --- a/docs/en/users/03_Main_view.md +++ b/docs/en/users/03_Main_view.md @@ -32,20 +32,20 @@ Here is an example to trigger article update every hour. Special parameters to configure the script - all parameters can be combined: -- Parameter "force" -https://freshrss.example.net/i/?c=feed&a=actualize&force=1 +- Parameter "force" +https://freshrss.example.net/i/?c=feed&a=actualize&force=1 If *force* is set to 1 all feeds will be refreshed at once. -- Parameter "ajax" -https://freshrss.example.net/i/?c=feed&a=actualize&ajax=1 +- Parameter "ajax" +https://freshrss.example.net/i/?c=feed&a=actualize&ajax=1 Only a status site is returned and not a complete website. Example: "OK" -- Parameter "maxFeeds" -https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=30 +- Parameter "maxFeeds" +https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=30 If *maxFeeds* is set the configured amount of feeds is refreshed at once. The default setting is "10". -- Parameter "token" -https://freshrss.example.net/i/?c=feed&a=actualize&token=542345872345734 +- Parameter "token" +https://freshrss.example.net/i/?c=feed&a=actualize&token=542345872345734 Security parameter to prevent unauthorized refreshes. For detailed Documentation see "Form authentication". ### Online cron diff --git a/docs/en/users/05_Configuration.md b/docs/en/users/05_Configuration.md index 225c1e5f9..f635f9d5e 100644 --- a/docs/en/users/05_Configuration.md +++ b/docs/en/users/05_Configuration.md @@ -9,7 +9,7 @@ the missing bits or add a new language, please check how you can [contribute to There are parts of FreshRSS that are not translated and are not intended to be translated. For now, the logs visible in the application as well as the one generated by automatic update scripts are part of it. -Available languages are: cz, de, en, es, fr, he, it, kr, nl, oc, pt-br, ru, tr, zh-cn. +Available languages are: cz, de, en, es, fr, he, it, kr, nl, oc, pt-br, ru, tr, zh-cn. ## Theme diff --git a/docs/en/users/07_Frequently_Asked_Questions.md b/docs/en/users/07_Frequently_Asked_Questions.md index 42156b1a9..fd3db4bea 100644 --- a/docs/en/users/07_Frequently_Asked_Questions.md +++ b/docs/en/users/07_Frequently_Asked_Questions.md @@ -47,7 +47,7 @@ For more information on that matter, there is a [dedicated documentation](../../ ## Permissions under SELinux -Some Linux distribution like Fedora or RedHat Enterprise Linux have SELinux system enabled. This acts like a firewall application, so all applications cannot write/modify files under certain conditions. While installing FreshRSS, step 2 can fail if the httpd process cannot write to some data sub-directories, the following command should be executed as root : +Some Linux distribution like Fedora or RedHat Enterprise Linux have SELinux system enabled. This acts like a firewall application, so all applications cannot write/modify files under certain conditions. While installing FreshRSS, step 2 can fail if the httpd process cannot write to some data sub-directories, the following command should be executed as root : ```sh semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/FreshRSS/data(/.*)?' restorecon -Rv /usr/share/FreshRSS/data diff --git a/docs/fr/developers/01_First_steps.md b/docs/fr/developers/01_First_steps.md index dd38bcb3f..df3fa65f2 100644 --- a/docs/fr/developers/01_First_steps.md +++ b/docs/fr/developers/01_First_steps.md @@ -57,7 +57,7 @@ Chaque opérateur est entouré d'espaces. ```php if ($a == 10) { - // faire quelque chose + // faire quelque chose } echo $a ? 1 : 0; @@ -69,11 +69,11 @@ Il n'y a pas d'espaces entre des parenthèses. Il n'y a pas d'espaces avant une ```php if ($a == 10) { - // faire quelque chose + // faire quelque chose } if ((int)$a == 10) { - // faire quelque chose + // faire quelque chose } ``` @@ -84,16 +84,16 @@ Ce cas se présente le plus souvent en Javascript. Quand on a des fonctions chai ```javascript // Première instruction shortcut.add(shortcuts.mark_read, function () { - //... - }, { - 'disable_in_input': true - }); + //... + }, { + 'disable_in_input': true + }); // Deuxième instruction shortcut.add("shift+" + shortcuts.mark_read, function () { - //... - }, { - 'disable_in_input': true - }); + //... + }, { + 'disable_in_input': true + }); ``` ## Longueur des lignes @@ -105,7 +105,7 @@ Dans le cas des fonctions, les paramètres peuvent être déclarés sur plusieur ```php function ma_fonction($param_1, $param_2, $param_3, $param_4) { - // faire quelque chose + // faire quelque chose } ``` @@ -120,7 +120,7 @@ Les fonctions et les variables doivent suivre la convention "snake case". ```php // une fontion function nom_de_la_fontion() { - // faire quelque chose + // faire quelque chose } // une variable $nom_de_la_variable; @@ -132,7 +132,7 @@ Les méthodes doivent suivre la convention "lower camel case". ```php private function nomDeLaMethode() { - // faire quelque chose + // faire quelque chose } ``` @@ -160,7 +160,7 @@ Les opérateurs doivent être en fin de ligne dans le cas de conditions sur plus ```php if ($a == 10 || $a == 20) { - // faire quelque chose + // faire quelque chose } ``` @@ -173,9 +173,9 @@ Si le fichier ne contient que du PHP, il ne doit pas comporter de balise fermant Lors de l'écriture de tableaux sur plusieurs lignes, tous les éléments doivent être suivis d'une virgule (même le dernier). ```php -$variable = array( - "valeur 1", - "valeur 2", - "valeur 3", -); +$variable = [ + "valeur 1", + "valeur 2", + "valeur 3", +]; ``` diff --git a/docs/fr/developers/03_Backend/05_Extensions.md b/docs/fr/developers/03_Backend/05_Extensions.md index 2ee81b781..3a23e0c5a 100644 --- a/docs/fr/developers/03_Backend/05_Extensions.md +++ b/docs/fr/developers/03_Backend/05_Extensions.md @@ -49,13 +49,13 @@ Exemple de code : view->a_variable = 'FooBar'; - } + public function indexAction() { + $this->view->a_variable = 'FooBar'; + } - public function worldAction() { - $this->view->a_variable = 'Hello World!'; - } + public function worldAction() { + $this->view->a_variable = 'Hello World!'; + } } ?> @@ -75,7 +75,7 @@ Comme expliqué plus haut, les vues sont du code HTML mixé à du PHP. Exemple d ```html

    - Phrase passée en paramètre : a_variable; ?> + Phrase passée en paramètre : a_variable; ?>

    ``` @@ -119,7 +119,7 @@ Pour profiter pleinement du système de routage de Minz, il est fortement décon ```html

    - Accéder à la page Hello world! + Accéder à la page Hello world!

    ``` @@ -130,13 +130,13 @@ Préférez donc l'utilisation de la classe `Minz_Url` et de sa méthode `display ```php 'hello', - 'a' => 'world', - 'params' => array( - 'foo' => 'bar', - ) -); +$url_array = [ + 'c' => 'hello', + 'a' => 'world', + 'params' => [ + 'foo' => 'bar', + ], +]; // Affichera quelque chose comme .?c=hello&a=world&foo=bar echo Minz_Url::display($url_array); @@ -166,10 +166,10 @@ Exemple de code : ```php 'hello', - 'a' => 'world' -); +$url_array = [ + 'c' => 'hello', + 'a' => 'world', +]; // Indique à Minz de rediriger l'utilisateur vers la page hello/world. // Notez qu'il s'agit d'une redirection au sens Minz du terme, pas d'une redirection que le navigateur va avoir à gérer (code HTTP 301 ou 302) @@ -188,10 +188,10 @@ Il est très fréquent de vouloir effectuer une redirection tout en affichant un ```php 'hello', - 'a' => 'world' -); +$url_array = [ + 'c' => 'hello', + 'a' => 'world', +]; $feedback_good = 'Tout s\'est bien passé !'; $feedback_bad = 'Oups, quelque chose n\'a pas marché.'; @@ -225,19 +225,19 @@ Les fichiers de traduction sont assez simples : il s'agit seulement de retourne ```php array( - 'actualize' => 'Actualiser', - 'back_to_rss_feeds' => '← Retour à vos flux RSS', - 'cancel' => 'Annuler', - 'create' => 'Créer', - 'disable' => 'Désactiver', - ), - 'freshrss' => array( - '_' => 'FreshRSS', - 'about' => 'À propos de FreshRSS', - ), -); +return [ + 'action' => [ + 'actualize' => 'Actualiser', + 'back_to_rss_feeds' => '← Retour à vos flux RSS', + 'cancel' => 'Annuler', + 'create' => 'Créer', + 'disable' => 'Désactiver', + ], + 'freshrss' => [ + '_' => 'FreshRSS', + 'about' => 'À propos de FreshRSS', + ], +]; ?> ``` @@ -246,9 +246,9 @@ Pour accéder à ces traductions, `Minz_Translate` va nous aider à l'aide de sa ```html

    - - - + + +

    ``` diff --git a/docs/fr/users/06_Mobile_access.md b/docs/fr/users/06_Mobile_access.md index 9f8c64f5c..f637ccf5b 100644 --- a/docs/fr/users/06_Mobile_access.md +++ b/docs/fr/users/06_Mobile_access.md @@ -45,7 +45,7 @@ Voir la [page sur l’API compatible Fever](06_Fever_API.md) pour une autre poss 6. Vous pouvez maintenant tester sur une application mobile (News+, FeedMe, ou EasyRSS sur Android) * en utilisant comme adresse https://rss.example.net/api/greader.php ou http://example.net/FreshRSS/p/api/greader.php selon la configuration de votre site Web. - * ⚠️ attention aux majuscules et aux espaces en tapant l’adresse avec le clavier du mobile ⚠️ + * ⚠️ attention aux majuscules et aux espaces en tapant l’adresse avec le clavier du mobile ⚠️ * avec votre nom d’utilisateur et le mot de passe enregistré au point 2 (mot de passe API). @@ -53,7 +53,7 @@ Voir la [page sur l’API compatible Fever](06_Fever_API.md) pour une autre poss * Vous pouvez voir les logs API dans `./FreshRSS/data/users/_/log_api.txt` * Si vous avez une erreur 404 (fichier non trouvé) lors de l’étape de test, et que vous êtes sous Apache, - voir http://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes pour utiliser News+ + voir http://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes pour utiliser News+ (facultatif pour EasyRSS et FeedMe qui devraient fonctionner dès lors que vous obtenez un PASS au test *Check partial server configuration*). diff --git a/docs/fr/users/07_Frequently_Asked_Questions.md b/docs/fr/users/07_Frequently_Asked_Questions.md index 2dc2cae97..87ff8631a 100644 --- a/docs/fr/users/07_Frequently_Asked_Questions.md +++ b/docs/fr/users/07_Frequently_Asked_Questions.md @@ -19,9 +19,9 @@ L'explication est la même pour les fichiers ```favicon.ico``` et ```.htaccess`` ## Pourquoi j'ai des erreurs quand j'essaye d'enregistrer un flux ? -Il peut y avoir différentes origines à ce problème. +Il peut y avoir différentes origines à ce problème. Le flux peut avoir une syntaxe invalide, il peut ne pas être reconnu par la bibliothèque SimplePie, l'hébergement peut avoir des problèmes, FreshRSS peut être boggué. -Il faut dans un premier temps déterminer la cause du problème. +Il faut dans un premier temps déterminer la cause du problème. Voici la liste des étapes à suivre pour la déterminer : 1. __Vérifier la validité du flux__ grâce à l'[outil en ligne du W3C](http://validator.w3.org/feed/ "Validateur en ligne de flux RSS et Atom"). Si ça ne fonctionne pas, nous ne pouvons rien faire. diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index 3fabb73c8..873fa21ff 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -117,7 +117,7 @@ abstract class MinzPDO extends PDO { public function __construct($dsn, $username = null, $passwd = null, $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); - } + } abstract public function dbType(); @@ -178,7 +178,7 @@ class MinzPDOSQLite extends MinzPDO { public function __construct($dsn, $username = null, $passwd = null, $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->exec('PRAGMA foreign_keys = ON;'); - } + } public function dbType() { return 'sqlite'; @@ -193,7 +193,7 @@ class MinzPDOPGSQL extends MinzPDO { public function __construct($dsn, $username = null, $passwd = null, $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->exec("SET NAMES 'UTF8';"); - } + } public function dbType() { return 'pgsql'; -- cgit v1.2.3 From cc0db9af4f980829faa4bf0960617807b32fb4fa Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 23 Oct 2019 00:52:15 +0200 Subject: Feature/new archiving (#2335) * Change archiving config page layout I've changed some wording and moved actions into a maintenance section. * Update purge action Now we have more control on the purge action. The configuration allows us to choose what to keep and what to discard in a more precise way. At the moment, the configuration applies for all feeds. * Add purge configuration on feed level Now the extend purge configuration is available on feed level. It is stored as attributes and will be used in the purge action. * Update purge action Now the purge action uses the feed configuration if it exists and defaults on user configuration if not. * Add empty option in period list * Fix configuration warnings * Add archiving configuration on categories See #2369 * Add user info back * Add explanations in UI * Fixes for SQLite + error + misc. * Fix invalid feed reference * Short array syntax Only for new code, so far * Fix prefix error * Query performance, default values Work in progress * Fix default values and confirm before leaving Form cancel and confirm changes before leaving were broken. And start taking advantage of the short echo syntax `` as we have moved to PHP 5.4+ * More work * Tuning SQL * Fix MariaDB + performance issue * SQL performance * Fix SQLite bug * Fix some attributes JSON encoding bugs Especially for SQLite export/import * More uniform, fix bugs More uniform between global, category, feed settings * Drop special cases for old articles during refresh Instead will use lastSeen date with the new archiving logic. This was generating problems anyway https://github.com/FreshRSS/FreshRSS/issues/2154 * Draft drop index keep_history Not needed anymore * MySQL typo Now properly tested with MySQL, PostgreSQL, SQLite * More work for legacy values Important to avoid overriding user's preference and risking deleting data erroneously * Fix PHP 7.3 / 7.4 warnings @aledeg "Trying to use values of type null, bool, int, float or resource as an array (such as $null["key"]) will now generate a notice. " https://php.net/migration74.incompatible * Reintroduce min articles and take care of legacy parameters * A few changes forgotten * Draft of migration + DROP of feed.keep_history * Fix several errors And give up using const for SQL to allow multiple database types (and we cannot redefine a const) * Add keep_min to categories + factorise archiving logic * Legacy fix * Fix bug yield from * Minor: Use JSON_UNESCAPED_SLASHE for attributes And make more uniform * Fix sign and missing variable * Fine tune the logic --- app/Controllers/configureController.php | 43 +++++++++- app/Controllers/entryController.php | 20 +---- app/Controllers/feedController.php | 24 +----- app/Controllers/subscriptionController.php | 58 +++++++++++++- app/Models/Category.php | 24 ++++++ app/Models/CategoryDAO.php | 122 ++++++++++++++++++++++++++--- app/Models/CategoryDAOSQLite.php | 17 ++++ app/Models/ConfigurationSetter.php | 10 --- app/Models/Context.php | 19 +++++ app/Models/DatabaseDAO.php | 12 +-- app/Models/DatabaseDAOSQLite.php | 2 +- app/Models/EntryDAO.php | 65 ++++++++++----- app/Models/Factory.php | 8 +- app/Models/Feed.php | 38 ++++++--- app/Models/FeedDAO.php | 20 ++--- app/Models/FeedDAOSQLite.php | 2 +- app/Models/Tag.php | 2 +- app/Models/TagDAO.php | 14 +++- app/Models/UserDAO.php | 11 ++- app/SQL/install.sql.mysql.php | 21 +++-- app/SQL/install.sql.pgsql.php | 19 +++-- app/SQL/install.sql.sqlite.php | 17 ++-- app/i18n/cz/conf.php | 12 ++- app/i18n/cz/gen.php | 7 ++ app/i18n/cz/sub.php | 3 +- app/i18n/de/conf.php | 12 ++- app/i18n/de/gen.php | 7 ++ app/i18n/de/sub.php | 3 +- app/i18n/en/conf.php | 12 ++- app/i18n/en/gen.php | 7 ++ app/i18n/en/sub.php | 3 +- app/i18n/es/conf.php | 12 ++- app/i18n/es/gen.php | 7 ++ app/i18n/es/sub.php | 3 +- app/i18n/fr/conf.php | 12 ++- app/i18n/fr/gen.php | 7 ++ app/i18n/fr/sub.php | 3 +- app/i18n/he/conf.php | 12 ++- app/i18n/he/gen.php | 7 ++ app/i18n/he/sub.php | 3 +- app/i18n/it/conf.php | 12 ++- app/i18n/it/gen.php | 7 ++ app/i18n/it/sub.php | 3 +- app/i18n/kr/conf.php | 12 ++- app/i18n/kr/gen.php | 7 ++ app/i18n/kr/sub.php | 3 +- app/i18n/nl/conf.php | 16 +++- app/i18n/nl/gen.php | 7 ++ app/i18n/nl/sub.php | 3 +- app/i18n/oc/conf.php | 11 ++- app/i18n/oc/gen.php | 7 ++ app/i18n/oc/sub.php | 3 +- app/i18n/pt-br/conf.php | 12 ++- app/i18n/pt-br/gen.php | 7 ++ app/i18n/pt-br/sub.php | 3 +- app/i18n/ru/conf.php | 14 +++- app/i18n/ru/gen.php | 7 ++ app/i18n/ru/sub.php | 3 +- app/i18n/sk/conf.php | 2 +- app/i18n/sk/sub.php | 2 +- app/i18n/tr/conf.php | 12 ++- app/i18n/tr/gen.php | 7 ++ app/i18n/tr/sub.php | 3 +- app/i18n/zh-cn/conf.php | 12 ++- app/i18n/zh-cn/gen.php | 7 ++ app/i18n/zh-cn/sub.php | 3 +- app/install.php | 20 +---- app/views/configure/archiving.phtml | 110 ++++++++++++++++++++------ app/views/helpers/category/update.phtml | 116 +++++++++++++++++++++++++++ app/views/helpers/feed/update.phtml | 113 ++++++++++++++++++++++++-- cli/_update-or-create-user.php | 4 +- config-user.default.php | 10 ++- lib/Minz/Request.php | 6 ++ lib/lib_rss.php | 6 +- p/scripts/category.js | 23 ++++++ p/scripts/extra.js | 38 ++++++--- p/themes/base-theme/template.css | 3 + phpcs.xml | 5 -- 78 files changed, 1062 insertions(+), 277 deletions(-) create mode 100644 app/Models/CategoryDAOSQLite.php (limited to 'cli') diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 85ca9da39..b38d3289a 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -196,9 +196,31 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function archivingAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3); - FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0); + if (!Minz_Request::paramBoolean('enable_keep_max')) { + $keepMax = false; + } elseif (!$keepMax = Minz_Request::param('keep_max')) { + $keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT; + } + if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) { + $keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD; + if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) { + $keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit')); + } + } else { + $keepPeriod = false; + } + FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', FreshRSS_Feed::TTL_DEFAULT); + FreshRSS_Context::$user_conf->archiving = [ + 'keep_period' => $keepPeriod, + 'keep_max' => $keepMax, + 'keep_min' => Minz_Request::param('keep_min_default', 0), + 'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'), + 'keep_labels' => Minz_Request::paramBoolean('keep_labels'), + 'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'), + ]; + FreshRSS_Context::$user_conf->keep_history_default = null; //Legacy < FreshRSS 1.15 + FreshRSS_Context::$user_conf->old_entries = null; //Legacy < FreshRSS 1.15 FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); @@ -206,7 +228,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController { array('c' => 'configure', 'a' => 'archiving')); } - Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); + $volatile = [ + 'enable_keep_period' => false, + 'keep_period_count' => '3', + 'keep_period_unit' => 'P1M', + ]; + $keepPeriod = FreshRSS_Context::$user_conf->archiving['keep_period']; + if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $keepPeriod, $matches)) { + $volatile = [ + 'enable_keep_period' => true, + 'keep_period_count' => $matches['count'], + 'keep_period_unit' => str_replace($matches['count'], 1, $keepPeriod), + ]; + } + FreshRSS_Context::$user_conf->volatile = $volatile; $entryDAO = FreshRSS_Factory::createEntryDao(); $this->view->nb_total = $entryDAO->count(); @@ -217,6 +252,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController { if (FreshRSS_Auth::hasAccess('admin')) { $this->view->size_total = $databaseDAO->size(true); } + + Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); } /** diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 0215128f4..7881cb3ec 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -181,32 +181,20 @@ class FreshRSS_entry_Controller extends Minz_ActionController { public function purgeAction() { @set_time_limit(300); - $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - - $entryDAO = FreshRSS_Factory::createEntryDao(); $feedDAO = FreshRSS_Factory::createFeedDao(); $feeds = $feedDAO->listFeeds(); $nb_total = 0; invalidateHttpCache(); - foreach ($feeds as $feed) { - $feed_history = $feed->keepHistory(); - if (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) { - $feed_history = FreshRSS_Context::$user_conf->keep_history_default; - } + $feedDAO->beginTransaction(); - if ($feed_history >= 0) { - $nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, $feed_history); - if ($nb > 0) { - $nb_total += $nb; - Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']'); - } - } + foreach ($feeds as $feed) { + $nb_total += $feed->cleanOldEntries(); } $feedDAO->updateCachedValues(); + $feedDAO->commit(); $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); $databaseDAO->minorDbMaintenance(); diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index ea07d96e4..aabeb80ff 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -267,10 +267,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $maxFeeds = 10; } - // Calculate date of oldest entries we accept in DB. - $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - // WebSub (PubSubHubbub) support $pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled; $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. @@ -323,12 +319,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } - $feed_history = $feed->keepHistory(); - if ($isNewFeed) { - $feed_history = FreshRSS_Feed::KEEP_HISTORY_INFINITE; - } elseif (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) { - $feed_history = FreshRSS_Context::$user_conf->keep_history_default; - } $needFeedCacheRefresh = false; // We want chronological order and SimplePie uses reverse order. @@ -376,15 +366,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $entryDAO->updateEntry($entry->toArray()); } - } elseif ($feed_history == 0 && $entry_date < $date_min) { - // This entry should not be added considering configuration and date. - $oldGuids[] = $entry->guid(); } else { $id = uTimeString(); $entry->_id($id); - if ($entry_date < $date_min) { - $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read - } $entry->applyFilterActions(); @@ -413,17 +397,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime); } - if ($feed_history >= 0 && mt_rand(0, 30) === 1) { - // TODO: move this function in web cron when available (see entry::purge) - // Remove old entries once in 30. + if (mt_rand(0, 30) === 1) { // Remove old entries once in 30. if (!$entryDAO->inTransaction()) { $entryDAO->beginTransaction(); } - - $nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, max($feed_history, count($entries) + 10)); + $nb = $feed->cleanOldEntries(); if ($nb > 0) { $needFeedCacheRefresh = true; - Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']'); } } diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index f6d5e9457..f9497f0be 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -121,6 +121,32 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $feed->_attributes('timeout', null); } + if (Minz_Request::paramBoolean('use_default_purge_options')) { + $feed->_attributes('archiving', null); + } else { + if (!Minz_Request::paramBoolean('enable_keep_max')) { + $keepMax = false; + } elseif (!$keepMax = Minz_Request::param('keep_max')) { + $keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT; + } + if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) { + $keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD; + if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) { + $keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit')); + } + } else { + $keepPeriod = false; + } + $feed->_attributes('archiving', [ + 'keep_period' => $keepPeriod, + 'keep_max' => $keepMax, + 'keep_min' => intval(Minz_Request::param('keep_min', 0)), + 'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'), + 'keep_labels' => Minz_Request::paramBoolean('keep_labels'), + 'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'), + ]); + } + $feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', ''))); $values = array( @@ -132,7 +158,6 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { 'pathEntries' => Minz_Request::param('path_entries', ''), 'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)), 'httpAuth' => $httpAuth, - 'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)), 'ttl' => $ttl * ($mute ? -1 : 1), 'attributes' => $feed->attributes(), ); @@ -165,9 +190,36 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $this->view->category = $category; if (Minz_Request::isPost()) { - $values = array( + if (Minz_Request::paramBoolean('use_default_purge_options')) { + $category->_attributes('archiving', null); + } else { + if (!Minz_Request::paramBoolean('enable_keep_max')) { + $keepMax = false; + } elseif (!$keepMax = Minz_Request::param('keep_max')) { + $keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT; + } + if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) { + $keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD; + if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) { + $keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit')); + } + } else { + $keepPeriod = false; + } + $category->_attributes('archiving', [ + 'keep_period' => $keepPeriod, + 'keep_max' => $keepMax, + 'keep_min' => intval(Minz_Request::param('keep_min', 0)), + 'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'), + 'keep_labels' => Minz_Request::paramBoolean('keep_labels'), + 'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'), + ]); + } + + $values = [ 'name' => Minz_Request::param('name', ''), - ); + 'attributes' => $category->attributes(), + ]; invalidateHttpCache(); diff --git a/app/Models/Category.php b/app/Models/Category.php index 29c0e586b..a0ee1ddaa 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -8,6 +8,7 @@ class FreshRSS_Category extends Minz_Model { private $feeds = null; private $hasFeedsWithError = false; private $isDefault = false; + private $attributes = []; public function __construct($name = '', $feeds = null) { $this->_name($name); @@ -68,6 +69,14 @@ class FreshRSS_Category extends Minz_Model { return $this->hasFeedsWithError; } + public function attributes($key = '') { + if ($key == '') { + return $this->attributes; + } else { + return isset($this->attributes[$key]) ? $this->attributes[$key] : null; + } + } + public function _id($id) { $this->id = $id; if ($id == FreshRSS_CategoryDAO::DEFAULTCATEGORYID) { @@ -87,4 +96,19 @@ class FreshRSS_Category extends Minz_Model { $this->feeds = $values; } + + public function _attributes($key, $value) { + if ($key == '') { + if (is_string($value)) { + $value = json_decode($value, true); + } + if (is_array($value)) { + $this->attributes = $value; + } + } elseif ($value === null) { + unset($this->attributes[$key]); + } else { + $this->attributes[$key] = $value; + } + } } diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index dd49b542d..1b8717e83 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -4,15 +4,81 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable const DEFAULTCATEGORYID = 1; + protected function addColumn($name) { + Minz_Log::warning(__method__ . ': ' . $name); + try { + if ('attributes' === $name) { //v1.15.0 + $ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false; + + $stm = $this->pdo->query('SELECT * FROM `_feed`'); + $feeds = $stm->fetchAll(PDO::FETCH_ASSOC); + + $stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id'); + foreach ($feeds as $feed) { + if (empty($feed['keep_history']) || empty($feed['id'])) { + continue; + } + $keepHistory = $feed['keep_history']; + $attributes = empty($feed['attributes']) ? [] : json_decode($feed['attributes'], true); + if (is_string($attributes)) { //Legacy risk of double-encoding + $attributes = json_decode($attributes, true); + } + if (!is_array($attributes)) { + $attributes = []; + } + if ($keepHistory > 0) { + $attributes['archiving']['keep_min'] = intval($keepHistory); + } elseif ($keepHistory == -1) { //Infinite + $attributes['archiving']['keep_period'] = false; + $attributes['archiving']['keep_max'] = false; + $attributes['archiving']['keep_min'] = false; + } else { + continue; + } + $stm->bindValue(':id', $feed['id'], PDO::PARAM_INT); + $stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES)); + $stm->execute(); + } + + if ($this->pdo->dbType() !== 'sqlite') { //SQLite does not support DROP COLUMN + $this->pdo->exec('ALTER TABLE `_feed` DROP COLUMN keep_history'); + } else { + $this->pdo->exec('DROP INDEX IF EXISTS feed_keep_history_index'); //SQLite at least drop index + } + return $ok; + } + } catch (Exception $e) { + Minz_Log::error(__method__ . ': ' . $e->getMessage()); + } + return false; + } + + protected function autoUpdateDb($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { + foreach (['attributes'] as $column) { + if (stripos($errorInfo[2], $column) !== false) { + return $this->addColumn($column); + } + } + } + } + return false; + } + public function addCategory($valuesTmp) { - $sql = 'INSERT INTO `_category`(name) ' - . 'SELECT * FROM (SELECT TRIM(?)) c2 ' //TRIM() to provide a type hint as text for PostgreSQL + $sql = 'INSERT INTO `_category`(name, attributes) ' + . 'SELECT * FROM (SELECT TRIM(?), ?) c2 ' //TRIM() to provide a type hint as text for PostgreSQL . 'WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = TRIM(?))'; //No tag of the same name $stm = $this->pdo->prepare($sql); $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'); + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } $values = array( $valuesTmp['name'], + is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES), $valuesTmp['name'], ); @@ -20,7 +86,10 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable return $this->pdo->lastInsertId('`_category_id_seq`'); } else { $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); - Minz_Log::error('SQL error addCategory: ' . $info[2]); + if ($this->autoUpdateDb($info)) { + return $this->addCategory($valuesTmp); + } + Minz_Log::error('SQL error addCategory: ' . json_encode($info)); return false; } } @@ -39,13 +108,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable } public function updateCategory($id, $valuesTmp) { - $sql = 'UPDATE `_category` SET name=? WHERE id=? ' + $sql = 'UPDATE `_category` SET name=?, attributes=? WHERE id=? ' . 'AND NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = ?)'; //No tag of the same name $stm = $this->pdo->prepare($sql); $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'); + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } $values = array( $valuesTmp['name'], + is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES), $id, $valuesTmp['name'], ); @@ -54,7 +127,10 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable return $stm->rowCount(); } else { $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); - Minz_Log::error('SQL error updateCategory: ' . $info[2]); + if ($this->autoUpdateDb($info)) { + return $this->updateCategory($valuesTmp); + } + Minz_Log::error('SQL error updateCategory: ' . json_encode($info)); return false; } } @@ -70,16 +146,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable return $stm->rowCount(); } else { $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); - Minz_Log::error('SQL error deleteCategory: ' . $info[2]); + Minz_Log::error('SQL error deleteCategory: ' . json_encode($info)); return false; } } public function selectAll() { - $sql = 'SELECT id, name FROM `_category`'; + $sql = 'SELECT id, name, attributes FROM `_category`'; $stm = $this->pdo->query($sql); - while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { - yield $row; + if ($stm != false) { + while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { + yield $row; + } + } else { + $info = $this->pdo->errorInfo(); + if ($this->autoUpdateDb($info)) { + foreach ($this->selectAll() as $category) { // `yield from` requires PHP 7+ + yield $category; + } + } + Minz_Log::error(__method__ . ' error: ' . json_encode($info)); + return false; } } @@ -116,7 +203,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable public function listCategories($prePopulateFeeds = true, $details = false) { if ($prePopulateFeeds) { - $sql = 'SELECT c.id AS c_id, c.name AS c_name, ' + $sql = 'SELECT c.id AS c_id, c.name AS c_name, c.attributes AS c_attributes, ' . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ') . 'FROM `_category` c ' . 'LEFT OUTER JOIN `_feed` f ON f.category=c.id ' @@ -124,9 +211,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable . 'GROUP BY f.id, c_id ' . 'ORDER BY c.name, f.name'; $stm = $this->pdo->prepare($sql); - $stm->bindValue(':priority_normal', FreshRSS_Feed::PRIORITY_NORMAL, PDO::PARAM_INT); - $stm->execute(); - return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC)); + $values = [ ':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL ]; + if ($stm && $stm->execute($values)) { + return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC)); + } else { + $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->listCategories($prePopulateFeeds, $details); + } + Minz_Log::error('SQL error listCategories: ' . json_encode($info)); + return false; + } } else { $sql = 'SELECT * FROM `_category` ORDER BY name'; $stm = $this->pdo->query($sql); @@ -282,6 +377,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable $dao['name'] ); $cat->_id($dao['id']); + $cat->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : ''); $cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id'])); $list[$key] = $cat; } diff --git a/app/Models/CategoryDAOSQLite.php b/app/Models/CategoryDAOSQLite.php new file mode 100644 index 000000000..e32545c90 --- /dev/null +++ b/app/Models/CategoryDAOSQLite.php @@ -0,0 +1,17 @@ +pdo->query("PRAGMA table_info('category')")) { + $columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1); + foreach (['attributes'] as $column) { + if (!in_array($column, $columns)) { + return $this->addColumn($column); + } + } + } + return false; + } + +} diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 963d37e2b..b1d271f41 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -79,11 +79,6 @@ class FreshRSS_ConfigurationSetter { $data['html5_notif_timeout'] = $value >= 0 ? $value : 0; } - private function _keep_history_default(&$data, $value) { - $value = intval($value); - $data['keep_history_default'] = $value >= FreshRSS_Feed::KEEP_HISTORY_INFINITE ? $value : 0; - } - // It works for system config too! private function _language(&$data, $value) { $value = strtolower($value); @@ -94,11 +89,6 @@ class FreshRSS_ConfigurationSetter { $data['language'] = $value; } - private function _old_entries(&$data, $value) { - $value = intval($value); - $data['old_entries'] = $value > 0 ? $value : 3; - } - private function _passwordHash(&$data, $value) { $data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; } diff --git a/app/Models/Context.php b/app/Models/Context.php index 95dc47c8c..878b72c69 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -51,6 +51,25 @@ class FreshRSS_Context { // Init configuration. self::$system_conf = Minz_Configuration::get('system'); self::$user_conf = Minz_Configuration::get('user'); + + //Legacy + $oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0); + if ($oldEntries > 0) { //Freshrss < 1.15 + $archiving['keep_period'] = 'P' . $oldEntries . 'M'; + } + + $keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5); + if ($keepMin != 0 && $keepMin > -5) { //Freshrss < 1.15 + $archiving = FreshRSS_Context::$user_conf->archiving; + if ($keepMin > 0) { + $archiving['keep_min'] = $keepMin; + } elseif ($keepMin == -1) { //Infinite + $archiving['keep_period'] = false; + $archiving['keep_max'] = false; + $archiving['keep_min'] = false; + } + FreshRSS_Context::$user_conf->archiving = $archiving; + } } /** diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index f6cd2f756..a36b469b1 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -15,11 +15,11 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { const LENGTH_INDEX_UNICODE = 191; public function create() { - require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); + require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); $db = FreshRSS_Context::$system_conf->db; try { - $sql = sprintf(SQL_CREATE_DB, empty($db['base']) ? '' : $db['base']); + $sql = sprintf($SQL_CREATE_DB, empty($db['base']) ? '' : $db['base']); return $this->pdo->exec($sql) !== false; } catch (PDOException $e) { $_SESSION['bd_error'] = $e->getMessage(); @@ -86,7 +86,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function feedIsCorrect() { return $this->checkTable('feed', array( 'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate', - 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes', + 'priority', 'pathEntries', 'httpAuth', 'error', 'ttl', 'attributes', 'cache_nbEntries', 'cache_nbUnreads', )); } @@ -164,11 +164,11 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function ensureCaseInsensitiveGuids() { $ok = true; if ($this->pdo->dbType() === 'mysql') { - include_once(APP_PATH . '/SQL/install.sql.mysql.php'); + include(APP_PATH . '/SQL/install.sql.mysql.php'); $ok = false; try { - $ok = $this->pdo->exec(SQL_UPDATE_GUID_LATIN1_BIN) !== false; //FreshRSS 1.12 + $ok = $this->pdo->exec($SQL_UPDATE_GUID_LATIN1_BIN) !== false; //FreshRSS 1.12 } catch (Exception $e) { $ok = false; Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage()); @@ -243,7 +243,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { Minz_ModelPdo::clean(); $userDAOSQLite = new FreshRSS_UserDAO('', $sqlite); - $categoryDAOSQLite = new FreshRSS_CategoryDAO('', $sqlite); + $categoryDAOSQLite = new FreshRSS_CategoryDAOSQLite('', $sqlite); $feedDAOSQLite = new FreshRSS_FeedDAOSQLite('', $sqlite); $entryDAOSQLite = new FreshRSS_EntryDAOSQLite('', $sqlite); $tagDAOSQLite = new FreshRSS_TagDAOSQLite('', $sqlite); diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php index b1473ab09..413e7ee09 100644 --- a/app/Models/DatabaseDAOSQLite.php +++ b/app/Models/DatabaseDAOSQLite.php @@ -66,6 +66,6 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { } public function optimize() { - return $this->exec('VACUUM') !== false; + return $this->pdo->exec('VACUUM') !== false; } } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 5ff3a5b70..6a8a25b3e 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -26,9 +26,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $this->pdo->commit(); } try { - require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); + require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); Minz_Log::warning('SQL CREATE TABLE entrytmp...'); - $ok = $this->pdo->exec(SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_INDEX_ENTRY_1) !== false; + $ok = $this->pdo->exec($SQL_CREATE_TABLE_ENTRYTMP . $SQL_CREATE_INDEX_ENTRY_1) !== false; } catch (Exception $ex) { Minz_Log::error(__method__ . ' error: ' . $ex->getMessage()); } @@ -544,32 +544,57 @@ SQL; return $affected; } - public function cleanOldEntries($id_feed, $date_min, $keep = 15) { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after - $sql = 'DELETE FROM `_entry` ' - . 'WHERE id_feed=:id_feed1 AND id<=:id_max ' - . 'AND is_favorite=0 ' //Do not remove favourites - . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `_entry` e3 WHERE e3.id_feed=:id_feed2) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance - . 'AND id NOT IN (SELECT id_entry FROM `_entrytag`) ' //Do not purge tagged entries - . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `_entry` e2 WHERE e2.id_feed=:id_feed3 ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' - $stm = $this->pdo->prepare($sql); + public function cleanOldEntries($id_feed, $options = []) { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after + $sql = 'DELETE FROM `_entry` WHERE id_feed = :id_feed1'; //No alias for MySQL / MariaDB + $params = []; + $params[':id_feed1'] = $id_feed; - if ($stm) { - $id_max = intval($date_min) . '000000'; - $stm->bindParam(':id_max', $id_max, PDO::PARAM_STR); - $stm->bindParam(':id_feed1', $id_feed, PDO::PARAM_INT); - $stm->bindParam(':id_feed2', $id_feed, PDO::PARAM_INT); - $stm->bindParam(':id_feed3', $id_feed, PDO::PARAM_INT); - $stm->bindParam(':keep', $keep, PDO::PARAM_INT); + //==Exclusions== + if (!empty($options['keep_favourites'])) { + $sql .= ' AND is_favorite = 0'; + } + if (!empty($options['keep_unreads'])) { + $sql .= ' AND is_read = 1'; + } + if (!empty($options['keep_labels'])) { + $sql .= ' AND NOT EXISTS (SELECT 1 FROM `_entrytag` WHERE id_entry = id)'; + } + if (!empty($options['keep_min']) && $options['keep_min'] > 0) { + $sql .= ' AND `lastSeen` < (SELECT e2.`lastSeen` FROM `_entry` e2 WHERE e2.id_feed = :id_feed2' + . ' ORDER BY e2.`lastSeen` DESC LIMIT 1 OFFSET :keep_min)'; + $params[':id_feed2'] = $id_feed; + $params[':keep_min'] = (int)$options['keep_min']; } + //Keep at least the articles seen at the last refresh + $sql .= ' AND `lastSeen` < (SELECT MAX(e3.`lastSeen`) FROM `_entry` e3 WHERE e3.id_feed = :id_feed3)'; + $params[':id_feed3'] = $id_feed; + + //==Inclusions== + $sql .= ' AND (1=0'; + if (!empty($options['keep_period'])) { + $sql .= ' OR `lastSeen` < :max_last_seen'; + $now = new DateTime('now'); + $now->sub(new DateInterval($options['keep_period'])); + $params[':max_last_seen'] = $now->format('U'); + } + if (!empty($options['keep_max']) && $options['keep_max'] > 0) { + $sql .= ' OR `lastSeen` <= (SELECT e4.`lastSeen` FROM `_entry` e4 WHERE e4.id_feed = :id_feed4' + . ' ORDER BY e4.`lastSeen` DESC LIMIT 1 OFFSET :keep_max)'; + $params[':id_feed4'] = $id_feed; + $params[':keep_max'] = (int)$options['keep_max']; + } + $sql .= ')'; + + $stm = $this->pdo->prepare($sql); - if ($stm && $stm->execute()) { + if ($stm && $stm->execute($params)) { return $stm->rowCount(); } else { $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); if ($this->autoUpdateDb($info)) { - return $this->cleanOldEntries($id_feed, $date_min, $keep); + return $this->cleanOldEntries($id_feed, $options); } - Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]); + Minz_Log::error(__method__ . ' error:' . json_encode($info)); return false; } } diff --git a/app/Models/Factory.php b/app/Models/Factory.php index 6f2ca2217..69885c205 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -7,7 +7,13 @@ class FreshRSS_Factory { } public static function createCategoryDao($username = null) { - return new FreshRSS_CategoryDAO($username); + $conf = Minz_Configuration::get('system'); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_CategoryDAOSQLite($username); + default: + return new FreshRSS_CategoryDAO($username); + } } public static function createFeedDao($username = null) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 8aee9d62f..0a45a1f4c 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -7,8 +7,8 @@ class FreshRSS_Feed extends Minz_Model { const TTL_DEFAULT = 0; - const KEEP_HISTORY_DEFAULT = -2; - const KEEP_HISTORY_INFINITE = -1; + const ARCHIVING_RETENTION_COUNT_LIMIT = 10000; + const ARCHIVING_RETENTION_PERIOD = 'P3M'; private $id = 0; private $url; @@ -24,9 +24,8 @@ class FreshRSS_Feed extends Minz_Model { private $pathEntries = ''; private $httpAuth = ''; private $error = false; - private $keep_history = self::KEEP_HISTORY_DEFAULT; private $ttl = self::TTL_DEFAULT; - private $attributes = array(); + private $attributes = []; private $mute = false; private $hash = null; private $lockPath = ''; @@ -110,9 +109,6 @@ class FreshRSS_Feed extends Minz_Model { public function inError() { return $this->error; } - public function keepHistory() { - return $this->keep_history; - } public function ttl() { return $this->ttl; } @@ -230,12 +226,6 @@ class FreshRSS_Feed extends Minz_Model { public function _error($value) { $this->error = (bool)$value; } - public function _keepHistory($value) { - $value = intval($value); - $value = min($value, 1000000); - $value = max($value, self::KEEP_HISTORY_DEFAULT); - $this->keep_history = $value; - } public function _ttl($value) { $value = intval($value); $value = min($value, 100000000); @@ -469,6 +459,28 @@ class FreshRSS_Feed extends Minz_Model { $this->entries = $entries; } + public function cleanOldEntries() { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after + $archiving = $this->attributes('archiving'); + if ($archiving == null) { + $catDAO = FreshRSS_Factory::createCategoryDao(); + $category = $catDAO->searchById($this->category()); + $archiving = $category == null ? null : $category->attributes('archiving'); + if ($archiving == null) { + $archiving = FreshRSS_Context::$user_conf->archiving; + } + } + if (is_array($archiving)) { + $entryDAO = FreshRSS_Factory::createEntryDao(); + $nb = $entryDAO->cleanOldEntries($this->id(), $archiving); + if ($nb > 0) { + $needFeedCacheRefresh = true; + Minz_Log::debug($nb . ' entries cleaned in feed [' . $this->url(false) . '] with: ' . json_encode($archiving)); + } + return $nb; + } + return false; + } + protected function cacheFilename() { return CACHE_PATH . '/' . md5($this->url) . '.spc'; } diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index d4a91c145..fa0001df7 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -17,7 +17,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { protected function autoUpdateDb($errorInfo) { if (isset($errorInfo[0])) { if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { - foreach (array('attributes') as $column) { + foreach (['attributes'] as $column) { if (stripos($errorInfo[2], $column) !== false) { return $this->addColumn($column); } @@ -41,12 +41,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { `pathEntries`, `httpAuth`, error, - keep_history, ttl, attributes ) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stm = $this->pdo->prepare($sql); $valuesTmp['url'] = safe_ascii($valuesTmp['url']); @@ -54,6 +53,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if (!isset($valuesTmp['pathEntries'])) { $valuesTmp['pathEntries'] = ''; } + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } $values = array( substr($valuesTmp['url'], 0, 511), @@ -66,9 +68,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { mb_strcut($valuesTmp['pathEntries'], 0, 511, 'UTF-8'), base64_encode($valuesTmp['httpAuth']), isset($valuesTmp['error']) ? intval($valuesTmp['error']) : 0, - isset($valuesTmp['keep_history']) ? intval($valuesTmp['keep_history']) : FreshRSS_Feed::KEEP_HISTORY_DEFAULT, isset($valuesTmp['ttl']) ? intval($valuesTmp['ttl']) : FreshRSS_Feed::TTL_DEFAULT, - isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', + is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES), ); if ($stm && $stm->execute($values)) { @@ -135,7 +136,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($key === 'httpAuth') { $valuesTmp[$key] = base64_encode($v); } elseif ($key === 'attributes') { - $valuesTmp[$key] = json_encode($v); + $valuesTmp[$key] = is_string($valuesTmp[$key]) ? $valuesTmp[$key] : json_encode($valuesTmp[$key], JSON_UNESCAPED_SLASHES); } } $set = substr($set, 0, -2); @@ -246,7 +247,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { public function selectAll() { $sql = 'SELECT id, url, category, name, website, description, `lastUpdate`, priority, ' - . '`pathEntries`, `httpAuth`, error, keep_history, ttl, attributes ' + . '`pathEntries`, `httpAuth`, error, ttl, attributes ' . 'FROM `_feed`'; $stm = $this->pdo->query($sql); while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { @@ -319,7 +320,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { */ public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) { $this->updateTTL(); - $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes ' + $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes ' . 'FROM `_feed` ' . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT . ' AND `lastUpdate` < (' . (time() + 60) @@ -407,7 +408,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=:id'; $stm = $this->pdo->prepare($sql); $stm->bindParam(':id', $id, PDO::PARAM_INT); - if (!($stm && $stm->execute($values))) { + if (!($stm && $stm->execute())) { $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); Minz_Log::error('SQL error truncate: ' . $info[2]); $this->pdo->rollBack(); @@ -448,7 +449,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : ''); $myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : ''); $myFeed->_error(isset($dao['error']) ? $dao['error'] : 0); - $myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT); $myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT); $myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : ''); $myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0); diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php index c56447df6..0f685867a 100644 --- a/app/Models/FeedDAOSQLite.php +++ b/app/Models/FeedDAOSQLite.php @@ -5,7 +5,7 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO { protected function autoUpdateDb($errorInfo) { if ($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) { $columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1); - foreach (array('attributes') as $column) { + foreach (['attributes'] as $column) { if (!in_array($column, $columns)) { return $this->addColumn($column); } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 3eb989cc1..0d50e356c 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -3,7 +3,7 @@ class FreshRSS_Tag extends Minz_Model { private $id = 0; private $name; - private $attributes = array(); + private $attributes = []; private $nbEntries = -1; private $nbUnread = -1; diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index 9c0f591c9..5882eee76 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -13,14 +13,14 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $this->pdo->commit(); } try { - require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); + require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); Minz_Log::warning('SQL ALTER GUID case sensitivity...'); $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); $databaseDAO->ensureCaseInsensitiveGuids(); Minz_Log::warning('SQL CREATE TABLE tag...'); - $ok = $this->pdo->exec(SQL_CREATE_TABLE_TAGS) !== false; + $ok = $this->pdo->exec($SQL_CREATE_TABLE_TAGS) !== false; } catch (Exception $e) { Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage()); } @@ -48,9 +48,12 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $stm = $this->pdo->prepare($sql); $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8'); + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } $values = array( $valuesTmp['name'], - isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', + is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES), $valuesTmp['name'], ); @@ -81,9 +84,12 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $stm = $this->pdo->prepare($sql); $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8'); + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } $values = array( $valuesTmp['name'], - isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', + is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES), $id, $valuesTmp['name'], ); diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php index 8e7e977d0..4e824cf01 100644 --- a/app/Models/UserDAO.php +++ b/app/Models/UserDAO.php @@ -2,14 +2,14 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { public function createUser($insertDefaultFeeds = false) { - require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); + require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); try { - $sql = SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS; + $sql = $SQL_CREATE_TABLES . $SQL_CREATE_TABLE_ENTRYTMP . $SQL_CREATE_TABLE_TAGS; $ok = $this->pdo->exec($sql) !== false; //Note: Only exec() can take multiple statements safely. if ($ok && $insertDefaultFeeds) { $default_feeds = FreshRSS_Context::$system_conf->default_feeds; - $stm = $this->pdo->prepare(SQL_INSERT_FEED); + $stm = $this->pdo->prepare($SQL_INSERT_FEED); foreach ($default_feeds as $feed) { $parameters = [ ':url' => $feed['url'], @@ -38,9 +38,8 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { fwrite(STDERR, 'Deleting SQL data for user “' . $this->current_user . "”…\n"); } - require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); - - $ok = $this->pdo->exec(SQL_DROP_TABLES) !== false; + require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); + $ok = $this->pdo->exec($SQL_DROP_TABLES) !== false; if ($ok) { return true; diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index 87b5d1989..1eabfae8b 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -1,12 +1,13 @@ array( '_' => 'Archivace', - 'advanced' => 'Pokročilé', 'delete_after' => 'Smazat články starší než', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Více možností je dostupných v nastavení jednotlivých kanálů', - 'keep_history_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Optimalizovat databázi', 'optimize_help' => 'Občasná údržba zmenší velikost databáze', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Vyčistit nyní', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Archivace', 'ttl' => 'Neaktualizovat častěji než', ), diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php index c6dabd555..de1456187 100644 --- a/app/i18n/cz/gen.php +++ b/app/i18n/cz/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Žádné nové články', 'previous' => 'Předchozí', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index b2bdf416b..eaaff9acd 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Kategorie', 'add' => 'Přidat kategorii', + 'archiving' => 'Archivace', 'empty' => 'Vyprázdit kategorii', 'information' => 'Informace', 'new' => 'Nová kategorie', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Informace', - 'keep_history' => 'Zachovat tento minimální počet článků', + 'keep_min' => 'Zachovat tento minimální počet článků', 'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do %s.', 'mute' => 'mute', //TODO - Translation 'no_selected' => 'Nejsou označeny žádné kanály.', diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index 99225da9c..89bbfc10e 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Archivierung', - 'advanced' => 'Erweitert', 'delete_after' => 'Entferne Artikel nach', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Feeds verfügbar.', - 'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Datenbank optimieren', 'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Jetzt bereinigen', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Archivierung', 'ttl' => 'Aktualisiere automatisch nicht öfter als', ), diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 6cc791d5e..e2dd2a251 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Es gibt keine weiteren Artikel', 'previous' => 'Vorherige', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index abc01b954..1227b5559 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Kategorie', 'add' => 'Eine Kategorie hinzufügen', + 'archiving' => 'Archivierung', 'empty' => 'Leere Kategorie', 'information' => 'Information', 'new' => 'Neue Kategorie', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Information', - 'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird', + 'keep_min' => 'Minimale Anzahl an Artikeln, die behalten wird', 'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie %s eingefügt.', 'mute' => 'Stumm schalten', 'no_selected' => 'Kein Feed ausgewählt.', diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php index 1078c736c..2d4e06550 100644 --- a/app/i18n/en/conf.php +++ b/app/i18n/en/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Archiving', - 'advanced' => 'Advanced', 'delete_after' => 'Remove articles after', + 'exception' => 'Purge exception', 'help' => 'More options are available in the individual feed settings', - 'keep_history_by_feed' => 'Minimum number of articles to keep by feed', + 'keep_favourites' => 'Never delete favourites', + 'keep_min_by_feed' => 'Minimum number of articles to keep by feed', + 'keep_labels' => 'Never delete labels', + 'keep_unreads' => 'Never delete unreads', + 'maintenance' => 'Maintenance', 'optimize' => 'Optimise database', 'optimize_help' => 'Do occasionally to reduce the size of the database', + 'policy' => 'Purge policy', + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', 'purge_now' => 'Purge now', + 'keep_max' => 'Maximum number of articles to keep', + 'keep_period' => 'Maximum age of articles to keep', 'title' => 'Archiving', 'ttl' => 'Do not automatically refresh more often than', ), diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index a6ddcbb60..fc1bd587a 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -163,6 +163,13 @@ return array( 'nothing_to_load' => 'There are no more articles', 'previous' => 'Previous', ), + 'period' => array( + 'days' => 'days', + 'hours' => 'hours', + 'months' => 'months', + 'weeks' => 'weeks', + 'years' => 'years', + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index fde01f9df..04ca793ec 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Category', 'add' => 'Add a category', + 'archiving' => 'Archiving', 'empty' => 'Empty category', 'information' => 'Information', 'new' => 'New category', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', ), 'information' => 'Information', - 'keep_history' => 'Minimum number of articles to keep', + 'keep_min' => 'Minimum number of articles to keep', 'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under %s.', 'mute' => 'mute', 'no_selected' => 'No feed selected.', diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php index 6aaad8d13..7a93a87de 100755 --- a/app/i18n/es/conf.php +++ b/app/i18n/es/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Archivo', - 'advanced' => 'Avanzado', 'delete_after' => 'Eliminar artículos tras', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Hay más opciones disponibles en los ajustes de la fuente', - 'keep_history_by_feed' => 'Número mínimo de artículos a conservar por fuente', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Número mínimo de artículos a conservar por fuente', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Optimizar la base de datos', 'optimize_help' => 'Ejecuta la optimización de vez en cuando para reducir el tamaño de la base de datos', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Limpiar ahora', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Archivo', 'ttl' => 'No actualizar automáticamente más de', ), diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php index 4affecc51..538ddc8fe 100755 --- a/app/i18n/es/gen.php +++ b/app/i18n/es/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'No hay más artículos', 'previous' => 'Anterior', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php index 7d33c59fa..96be76c6c 100755 --- a/app/i18n/es/sub.php +++ b/app/i18n/es/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Categoría', 'add' => 'Añadir a la categoría', + 'archiving' => 'Archivo', 'empty' => 'Vaciar categoría', 'information' => 'Información', 'new' => 'Nueva categoría', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Información', - 'keep_history' => 'Número mínimo de artículos a conservar', + 'keep_min' => 'Número mínimo de artículos a conservar', 'moved_category_deleted' => 'Al borrar una categoría todas sus fuentes pasan automáticamente a la categoría %s.', 'mute' => 'mute', //TODO - Translation 'no_selected' => 'No hay funentes seleccionadas.', diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php index dcd623b5a..020c94085 100644 --- a/app/i18n/fr/conf.php +++ b/app/i18n/fr/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Archivage', - 'advanced' => 'Avancé', 'delete_after' => 'Supprimer les articles après', + 'exception' => 'Exception de nettoyage', 'help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.', - 'keep_history_by_feed' => 'Nombre minimum d’articles à conserver par flux', + 'keep_favourites' => 'Ne jamais supprimer les articles favoris', + 'keep_min_by_feed' => 'Nombre minimum d’articles à conserver par flux', + 'keep_labels' => 'Ne jamais supprimer les articles étiquetés', + 'keep_unreads' => 'Ne jamais supprimer les articles non lus', + 'maintenance' => 'Maintenance', 'optimize' => 'Optimiser la base de données', 'optimize_help' => 'À faire de temps en temps pour réduire la taille de la BDD', + 'policy' => 'Politique de nettoyage', + 'policy_warning' => 'Si aucune politique de nettoyage n’est sélectionnée, tous les articles seront conservés.', 'purge_now' => 'Purger maintenant', + 'keep_max' => 'Nombre maximum d’articles à conserver', + 'keep_period' => 'Âge maximum des articles à conserver', 'title' => 'Archivage', 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', ), diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 01b66d316..a6875dd05 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Fin des articles', 'previous' => 'Précédent', ), + 'period' => array( + 'days' => 'jours', + 'hours' => 'heures', + 'months' => 'mois', + 'weeks' => 'semaines', + 'years' => 'années', + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index df44150c2..d09a19e5a 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Catégorie', 'add' => 'Ajouter une catégorie', + 'archiving' => 'Archivage', 'empty' => 'Catégorie vide', 'information' => 'Informations', 'new' => 'Nouvelle catégorie', @@ -40,7 +41,7 @@ return array( 'help' => 'Écrivez une recherche par ligne.', ), 'information' => 'Informations', - 'keep_history' => 'Nombre minimum d’articles à conserver', + 'keep_min' => 'Nombre minimum d’articles à conserver', 'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans %s.', 'mute' => 'muet', 'no_selected' => 'Aucun flux sélectionné.', diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php index 7e764b944..b987f21f4 100644 --- a/app/i18n/he/conf.php +++ b/app/i18n/he/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'ארכוב', - 'advanced' => 'מתקדם', 'delete_after' => 'מחיקת מאמרים לאחר', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'אפשרויות נוספות זמינות בזרמים ספציפיים', - 'keep_history_by_feed' => 'Minimum number of articles to keep by feed', //TODO - Translation + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Minimum number of articles to keep by feed', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'מיטוב בסיס הנתונים', 'optimize_help' => 'ביצוע לעיתים קרובות על מנת למטב את בסיס הנתונים', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'ניקוי עכשיו', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'ארכוב', 'ttl' => 'אין לרענן אוטומטית יותר מ', ), diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php index 158f11e5b..34e6d77de 100644 --- a/app/i18n/he/gen.php +++ b/app/i18n/he/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'אין מאמרים נוספים', 'previous' => 'הקודם', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php index 8a629defb..15965d9e2 100644 --- a/app/i18n/he/sub.php +++ b/app/i18n/he/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'קטגוריה', 'add' => 'הוספת קטגוריה', + 'archiving' => 'ארכוב', 'empty' => 'Empty category', //TODO - Translation 'information' => 'מידע', 'new' => 'קטגוריה חדשה', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'מידע', - 'keep_history' => 'מסםר מינימלי של מאמרים לשמור', + 'keep_min' => 'מסםר מינימלי של מאמרים לשמור', 'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת %s.', 'mute' => 'mute', //TODO - Translation 'no_selected' => 'אף הזנה לא נבחרה.', diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php index f06302c72..4bdaad33d 100644 --- a/app/i18n/it/conf.php +++ b/app/i18n/it/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Archiviazione', - 'advanced' => 'Avanzate', 'delete_after' => 'Rimuovi articoli dopo', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Altre opzioni sono disponibili nelle impostazioni dei singoli feed', - 'keep_history_by_feed' => 'Numero minimo di articoli da mantenere per feed', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Numero minimo di articoli da mantenere per feed', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Ottimizza database', 'optimize_help' => 'Da fare occasionalmente per ridurre le dimensioni del database', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Cancella ora', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Archiviazione', 'ttl' => 'Non effettuare aggiornamenti per più di', ), diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php index 604cc6941..50d4b4e6c 100644 --- a/app/i18n/it/gen.php +++ b/app/i18n/it/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Non ci sono altri articoli', 'previous' => 'Precedente', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php index 50738d9e3..22cd36986 100644 --- a/app/i18n/it/sub.php +++ b/app/i18n/it/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Categoria', 'add' => 'Aggiungi una categoria', + 'archiving' => 'Archiviazione', 'empty' => 'Categoria vuota', 'information' => 'Informazioni', 'new' => 'Nuova categoria', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Informazioni', - 'keep_history' => 'Numero minimo di articoli da mantenere', + 'keep_min' => 'Numero minimo di articoli da mantenere', 'moved_category_deleted' => 'Cancellando una categoria i feed al suo interno verranno classificati automaticamente come %s.', 'mute' => 'mute', //TODO - Translation 'no_selected' => 'Nessun feed selezionato.', diff --git a/app/i18n/kr/conf.php b/app/i18n/kr/conf.php index 397d57418..1e77d0098 100644 --- a/app/i18n/kr/conf.php +++ b/app/i18n/kr/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => '보관', - 'advanced' => '고급 설정', 'delete_after' => '다음 기간보다 오래된 글 삭제', + 'exception' => 'Purge exception', //TODO - Translation 'help' => '더 자세한 옵션은 개별 피드 설정에 있습니다', - 'keep_history_by_feed' => '피드별 최소 유지 글 개수', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => '피드별 최소 유지 글 개수', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => '데이터베이스 최적화', 'optimize_help' => '데이터베이스 크기를 줄이기 위해 가끔씩 수행해주세요', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => '지금 삭제', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => '보관', 'ttl' => '다음 시간이 지나기 전에 새로고침 금지', ), diff --git a/app/i18n/kr/gen.php b/app/i18n/kr/gen.php index 55fea3d66..fdc95d431 100644 --- a/app/i18n/kr/gen.php +++ b/app/i18n/kr/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => '더 이상 글이 없습니다', 'previous' => '이전', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/kr/sub.php b/app/i18n/kr/sub.php index f8eccfa27..2586395f2 100644 --- a/app/i18n/kr/sub.php +++ b/app/i18n/kr/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => '카테고리', 'add' => '카테고리 추가', + 'archiving' => '보관', 'empty' => '빈 카테고리', 'information' => '정보', 'new' => '새 카테고리', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => '정보', - 'keep_history' => '최소 유지 글 개수', + 'keep_min' => '최소 유지 글 개수', 'moved_category_deleted' => '카테고리를 삭제하면, 해당 카테고리 아래에 있던 피드들은 자동적으로 %s 아래로 분류됩니다.', 'mute' => '무기한 새로고침 금지', 'no_selected' => '선택된 피드가 없습니다.', diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php index ec219d051..22302ccc0 100644 --- a/app/i18n/nl/conf.php +++ b/app/i18n/nl/conf.php @@ -1,15 +1,23 @@ array( '_' => 'Archivering', - 'advanced' => 'Geavanceerd', 'delete_after' => 'Verwijder artikelen na', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Meer opties zijn beschikbaar in de persoonlijke stroom instellingen', - 'keep_history_by_feed' => 'Minimum aantal te behouden artikelen in de feed', - 'optimize' => 'Optimaliseer database', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Minimum aantal te behouden artikelen in de feed', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation + 'optimize' => 'Optimaliseer database', //TODO - Translation 'optimize_help' => 'Doe dit zo af en toe om de omvang van de database te verkleinen', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Schoon nu op', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Archivering', 'ttl' => 'Vernieuw niet automatisch meer dan', ), diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php index 0dcb3010a..4854e806e 100644 --- a/app/i18n/nl/gen.php +++ b/app/i18n/nl/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Er zijn geen artikelen meer', 'previous' => 'Vorige', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'email' => 'Email', 'Known' => 'Known-gebaseerde sites', diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index 8ceb5aa28..6b498132f 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Categorie', 'add' => 'Voeg categorie toe', + 'archiving' => 'Archiveren', 'empty' => 'Lege categorie', 'information' => 'Informatie', 'new' => 'Nieuwe categorie', @@ -40,7 +41,7 @@ return array( 'help' => 'Voer één zoekfilter per lijn in.', ), 'information' => 'Informatie', - 'keep_history' => 'Minimum aantal artikelen om te houden', + 'keep_min' => 'Minimum aantal artikelen om te houden', 'moved_category_deleted' => 'Als u een categorie verwijderd, worden de feeds automatisch geclassificeerd onder %s.', 'mute' => 'demp', 'no_selected' => 'Geen feed geselecteerd.', diff --git a/app/i18n/oc/conf.php b/app/i18n/oc/conf.php index 76c41911e..e8de3b089 100644 --- a/app/i18n/oc/conf.php +++ b/app/i18n/oc/conf.php @@ -5,11 +5,20 @@ return array( '_' => 'Archius', 'advanced' => 'Avançat', 'delete_after' => 'Levar los articles aprèp', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Mai d’opcions son disponiblas dins la configuracion individuala dels fluxes', - 'keep_history_by_feed' => 'Nombre minimum d’articles de servar per flux', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Nombre minimum d’articles de servar per flux', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Optimizar la basa de donada', 'optimize_help' => 'De far de temps en temps per redusir la talha de la basa de donadas', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Purgar ara', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Archius', 'ttl' => 'Actualizar pas automaticament mai sovent que', ), diff --git a/app/i18n/oc/gen.php b/app/i18n/oc/gen.php index 7ab56368f..928377997 100644 --- a/app/i18n/oc/gen.php +++ b/app/i18n/oc/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'I a pas mai d’articles', 'previous' => 'Precedent', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/oc/sub.php b/app/i18n/oc/sub.php index eae9dff29..0f465d7ca 100644 --- a/app/i18n/oc/sub.php +++ b/app/i18n/oc/sub.php @@ -12,6 +12,7 @@ return array( 'category' => array( '_' => 'Categoria', 'add' => 'Ajustar una categoria', + 'archiving' => 'Archivar', 'empty' => 'Categoria voida', 'information' => 'Informacions', 'new' => 'Nòva categoria', @@ -39,7 +40,7 @@ return array( 'help' => 'Escrivètz una recèrca per linha.', ), 'information' => 'Informacions', - 'keep_history' => 'Nombre minimum d’articles de servar', + 'keep_min' => 'Nombre minimum d’articles de servar', 'moved_category_deleted' => 'Quand escafatz una categoria, sos fluxes son automaticament classats dins %s.', 'mute' => 'mut', 'no_selected' => 'Cap de flux pas seleccionat.', diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php index eb067e58a..5e43cc373 100644 --- a/app/i18n/pt-br/conf.php +++ b/app/i18n/pt-br/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Arquivar', - 'advanced' => 'Avançado', 'delete_after' => 'Remover artigos depois', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Mais opções estão disponíveis nas configurações individuais do feed', - 'keep_history_by_feed' => 'Número mínimo de artigos para deixar no feed', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Número mínimo de artigos para deixar no feed', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Otimizar banco de dados', 'optimize_help' => 'Faça ocasionalmente para reduzir o tamanho do banco de dados', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Purge agora', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Arquivar', 'ttl' => 'Não atualize automaticamente mais frequente que', ), diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php index b327937d5..0e7f367ee 100644 --- a/app/i18n/pt-br/gen.php +++ b/app/i18n/pt-br/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Não há mais artigos', 'previous' => 'Anterior', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php index d4bea33c4..c4c28bd6c 100644 --- a/app/i18n/pt-br/sub.php +++ b/app/i18n/pt-br/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Categoria', 'add' => 'Adicionar uma categoria', + 'archiving' => 'Arquivar', 'empty' => 'Categoria vazia', 'information' => 'Informações', 'new' => 'Nova categoria', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Informações', - 'keep_history' => 'Número mínimo de artigos para manter', + 'keep_min' => 'Número mínimo de artigos para manter', 'moved_category_deleted' => 'Quando você deleta uma categoria, seus feeds são automaticamente classificados como %s.', 'mute' => 'mute', //TODO - Translation 'no_selected' => 'Nenhum feed selecionado.', diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php index af6f3b5f6..7a80587f8 100644 --- a/app/i18n/ru/conf.php +++ b/app/i18n/ru/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Архивация', - 'advanced' => 'Продвинутые настройки', 'delete_after' => 'Удалять статьи после', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Каждую подписку можно настроить более гибко', - 'keep_history_by_feed' => 'Minimum number of articles to keep by feed', //TODO - Translation + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Minimum number of articles to keep by feed', //TODO - Translation + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Оптимизировать базу данных', - 'optimize_help' => 'To do occasionally to reduce the size of the database', //TODO - Translation + 'optimize_help' => 'To do occasionally to reduce the size of the database', //TODO - Translation + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Очистить сейчас', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Архивация', 'ttl' => 'Не обновлять чаще чем', ), diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php index cc1d7e00e..5200a7005 100644 --- a/app/i18n/ru/gen.php +++ b/app/i18n/ru/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'There are no more articles', //TODO - Translation 'previous' => 'Previous', //TODO - Translation ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php index a2c4e4690..f4bda385d 100644 --- a/app/i18n/ru/sub.php +++ b/app/i18n/ru/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Category', //TODO - Translation 'add' => 'Add a category', //TODO - Translation + 'archiving' => 'Archivage', //TODO - Translation 'empty' => 'Empty category', //TODO - Translation 'information' => 'Information', //TODO - Translation 'new' => 'New category', //TODO - Translation @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Information', //TODO - Translation - 'keep_history' => 'Minimum number of articles to keep', //TODO - Translation + 'keep_min' => 'Minimum number of articles to keep', //TODO - Translation 'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under %s.', //TODO - Translation 'mute' => 'mute', //TODO - Translation 'no_selected' => 'No feed selected.', //TODO - Translation diff --git a/app/i18n/sk/conf.php b/app/i18n/sk/conf.php index f704fd4be..2e2289b79 100644 --- a/app/i18n/sk/conf.php +++ b/app/i18n/sk/conf.php @@ -6,7 +6,7 @@ return array( 'advanced' => 'Pokročilé', 'delete_after' => 'Vymazať články po', 'help' => 'Viac možností nájdete v nastaveniach kanála', - 'keep_history_by_feed' => 'Minimálny počet článkov kanála na zachovanie', + 'keep_min_by_feed' => 'Minimálny počet článkov kanála na zachovanie', 'optimize' => 'Optimalizovať databázu', 'optimize_help' => 'Občas vykonajte na zmenšenie veľkosti databázy', 'purge_now' => 'Vyčistiť teraz', diff --git a/app/i18n/sk/sub.php b/app/i18n/sk/sub.php index 4dcd09f57..2167e1817 100644 --- a/app/i18n/sk/sub.php +++ b/app/i18n/sk/sub.php @@ -40,7 +40,7 @@ return array( 'help' => 'Napíšte jeden výraz hľadania na riadok.', ), 'information' => 'Informácia', - 'keep_history' => 'Minimálny počet článkov na uchovanie', + 'keep_min' => 'Minimálny počet článkov na uchovanie', 'moved_category_deleted' => 'Keď vymažete kategóriu, jej kanály sa automaticky zaradia pod %s.', 'mute' => 'stíšiť', 'no_selected' => 'Nevybrali ste kanál.', diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php index 2bf1e8a6a..c8ea78efa 100644 --- a/app/i18n/tr/conf.php +++ b/app/i18n/tr/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => 'Arşiv', - 'advanced' => 'Gelişmiş', 'delete_after' => 'Makelelerin tutulacağı süre', + 'exception' => 'Purge exception', //TODO - Translation 'help' => 'Akış ayarlarında daha çok ayar bulabilirsiniz', - 'keep_history_by_feed' => 'Akışta en az tutulacak makale sayısı', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => 'Akışta en az tutulacak makale sayısı', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => 'Veritabanı optimize et', 'optimize_help' => 'Bu işlem bazen veritabanı boyutunu düşürmeye yardımcı olur', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => 'Şimdi temizle', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => 'Arşiv', 'ttl' => 'Şu süreden sık otomatik yenileme yapma', ), diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php index 5e361affb..ccc5b9ee6 100644 --- a/app/i18n/tr/gen.php +++ b/app/i18n/tr/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => 'Başka makale yok', 'previous' => 'Önceki', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php index 858d15758..f6f40d3f7 100644 --- a/app/i18n/tr/sub.php +++ b/app/i18n/tr/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => 'Kategori', 'add' => 'Kategori ekle', + 'archiving' => 'Arşiv', 'empty' => 'Boş kategori', 'information' => 'Bilgi', 'new' => 'Yeni kategori', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => 'Bilgi', - 'keep_history' => 'En az tutulacak makale sayısı', + 'keep_min' => 'En az tutulacak makale sayısı', 'moved_category_deleted' => 'Bir kategoriyi silerseniz, içerisindeki akışlar %s içerisine yerleşir.', 'mute' => 'mute', //TODO - Translation 'no_selected' => 'Hiçbir akış seçilmedi.', diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php index 2960cd6b1..a7404bc58 100644 --- a/app/i18n/zh-cn/conf.php +++ b/app/i18n/zh-cn/conf.php @@ -3,13 +3,21 @@ return array( 'archiving' => array( '_' => '存档', - 'advanced' => '高级', 'delete_after' => '文章保留', + 'exception' => 'Purge exception', //TODO - Translation 'help' => '详细选项位于单独的 RSS 源设置', - 'keep_history_by_feed' => '至少保存的文章数', + 'keep_favourites' => 'Never delete favourites', //TODO - Translation + 'keep_min_by_feed' => '至少保存的文章数', + 'keep_labels' => 'Never delete labels', //TODO - Translation + 'keep_unreads' => 'Never delete unreads', //TODO - Translation + 'maintenance' => 'Maintenance', //TODO - Translation 'optimize' => '优化数据库', 'optimize_help' => '偶尔执行优化可以减少数据库大小', + 'policy' => 'Purge policy', //TODO - Translation + 'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation 'purge_now' => '立即清除', + 'keep_max' => 'Maximum number of articles to keep', //TODO - Translation + 'keep_period' => 'Maximum age of articles to keep', //TODO - Translation 'title' => '存档', 'ttl' => '最小自动更新时间', ), diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php index 7ae156573..31817260e 100644 --- a/app/i18n/zh-cn/gen.php +++ b/app/i18n/zh-cn/gen.php @@ -162,6 +162,13 @@ return array( 'nothing_to_load' => '没有更多文章了', 'previous' => '上一页', ), + 'period' => array( + 'days' => 'days', //TODO - Translation + 'hours' => 'hours', //TODO - Translation + 'months' => 'months', //TODO - Translation + 'weeks' => 'weeks', //TODO - Translation + 'years' => 'years', //TODO - Translation + ), 'share' => array( 'blogotext' => 'Blogotext', 'diaspora' => 'Diaspora*', diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php index bf517756b..f6f3a0f7a 100644 --- a/app/i18n/zh-cn/sub.php +++ b/app/i18n/zh-cn/sub.php @@ -13,6 +13,7 @@ return array( 'category' => array( '_' => '分类', 'add' => '添加分类', + 'archiving' => '存档', 'empty' => '空分类', 'information' => '信息', 'new' => '新分类', @@ -40,7 +41,7 @@ return array( 'help' => 'Write one search filter per line.', //TODO - Translation ), 'information' => '信息', - 'keep_history' => '至少保存的文章数', + 'keep_min' => '至少保存的文章数', 'moved_category_deleted' => '删除分类时,其中的 RSS 源会自动归类到 %s', 'mute' => '暂停', 'no_selected' => '未选择 RSS 源。', diff --git a/app/install.php b/app/install.php index 366fb0cfc..f8bc6dd4e 100644 --- a/app/install.php +++ b/app/install.php @@ -86,7 +86,6 @@ function saveStep1() { // Then, we set $_SESSION vars $_SESSION['title'] = $system_conf->title; $_SESSION['auth_type'] = $system_conf->auth_type; - $_SESSION['old_entries'] = $user_conf->old_entries; $_SESSION['default_user'] = $current_user; $_SESSION['passwordHash'] = $user_conf->passwordHash; @@ -184,14 +183,12 @@ function saveStep3() { if (!empty($_POST)) { $system_default_config = Minz_Configuration::get('default_system'); $_SESSION['title'] = $system_default_config->title; - $_SESSION['old_entries'] = param('old_entries', $user_default_config->old_entries); $_SESSION['auth_type'] = param('auth_type', 'form'); if (FreshRSS_user_Controller::checkUsername(param('default_user', ''))) { $_SESSION['default_user'] = param('default_user', ''); } - if (empty($_SESSION['old_entries']) || - empty($_SESSION['auth_type']) || + if (empty($_SESSION['auth_type']) || empty($_SESSION['default_user'])) { return false; } @@ -208,10 +205,6 @@ function saveStep3() { FreshRSS_Context::$system_conf->default_user = $_SESSION['default_user']; FreshRSS_Context::$system_conf->save(); - if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) { - $_SESSION['old_entries'] = $user_default_config->old_entries; - } - // Create default user files but first, we delete previous data to // avoid access right problems. recursive_unlink(USERS_PATH . '/' . $_SESSION['default_user']); @@ -225,7 +218,6 @@ function saveStep3() { '', [ 'language' => $_SESSION['language'], - 'old_entries' => $_SESSION['old_entries'], ] ); } catch (Exception $e) { @@ -317,8 +309,7 @@ function checkStep2() { } function checkStep3() { - $conf = !empty($_SESSION['old_entries']) && - !empty($_SESSION['default_user']); + $conf = !empty($_SESSION['default_user']); $form = isset($_SESSION['auth_type']); @@ -593,13 +584,6 @@ function printStep3() {
    -
    - -
    - -
    -
    -
    diff --git a/app/views/configure/archiving.phtml b/app/views/configure/archiving.phtml index 09be55fd9..0387a2b96 100644 --- a/app/views/configure/archiving.phtml +++ b/app/views/configure/archiving.phtml @@ -8,23 +8,6 @@

    -
    - -
    - -   -
    -
    -
    - -
    - () -
    -
    @@ -47,6 +30,76 @@
    +

    + +

    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    @@ -55,9 +108,9 @@
    -
    + + -
    @@ -66,21 +119,30 @@
    -
    -
    - size_total); ?> +
    + -
    +
    + +
    -
    + + +
    + +
    + size_total); ?> +
    +
    +
    diff --git a/app/views/helpers/category/update.phtml b/app/views/helpers/category/update.phtml index b64bd786a..31482f163 100644 --- a/app/views/helpers/category/update.phtml +++ b/app/views/helpers/category/update.phtml @@ -33,5 +33,121 @@
    + + + category->attributes('archiving'); + if (empty($archiving)) { + $archiving = [ 'default' => true ]; + } else { + $archiving['default'] = false; + } + $volatile = [ + 'enable_keep_period' => false, + 'keep_period_count' => '3', + 'keep_period_unit' => 'P1M', + ]; + if (!empty($archiving['keep_period'])) { + if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) { + $volatile['enable_keep_period'] = true; + $volatile['keep_period_count'] = $matches['count']; + $volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']); + } + } + //Defaults + if (!isset($archiving['keep_max'])) { + $archiving['keep_max'] = false; + } + if (!isset($archiving['keep_favourites'])) { + $archiving['keep_favourites'] = true; + } + if (!isset($archiving['keep_labels'])) { + $archiving['keep_labels'] = true; + } + if (!isset($archiving['keep_unreads'])) { + $archiving['keep_unreads'] = false; + } + if (!isset($archiving['keep_min'])) { + $archiving['keep_min'] = 50; + } + ?> + +

    + +

    + +
    + +
    + +
    +
    + + + + + + +
    +
    + + +
    +
    diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 620806d7b..84461ed03 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -78,6 +78,7 @@
    +
    + feed->attributes('archiving'); + if (empty($archiving)) { + $archiving = [ 'default' => true ]; + } else { + $archiving['default'] = false; + } + $volatile = [ + 'enable_keep_period' => false, + 'keep_period_count' => '3', + 'keep_period_unit' => 'P1M', + ]; + if (!empty($archiving['keep_period'])) { + if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) { + $volatile['enable_keep_period'] = true; + $volatile['keep_period_count'] = $matches['count']; + $volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']); + } + } + //Defaults + if (!isset($archiving['keep_max'])) { + $archiving['keep_max'] = false; + } + if (!isset($archiving['keep_min'])) { + $archiving['keep_min'] = 50; + } + if (!isset($archiving['keep_favourites'])) { + $archiving['keep_favourites'] = true; + } + if (!isset($archiving['keep_labels'])) { + $archiving['keep_labels'] = true; + } + if (!isset($archiving['keep_unreads'])) { + $archiving['keep_unreads'] = false; + } + ?> + +

    + +

    +
    - +
    - +
    + + + + + + +
    @@ -143,6 +243,7 @@
    +
    diff --git a/cli/_update-or-create-user.php b/cli/_update-or-create-user.php index eda597f19..43b86a4a9 100644 --- a/cli/_update-or-create-user.php +++ b/cli/_update-or-create-user.php @@ -45,8 +45,8 @@ $values = array( 'language' => strParam('language'), 'mail_login' => strParam('email'), 'token' => strParam('token'), - 'old_entries' => intParam('purge_after_months'), - 'keep_history_default' => intParam('feed_min_articles_default'), + 'old_entries' => intParam('purge_after_months'), //TODO: Update with new mechanism + 'keep_history_default' => intParam('feed_min_articles_default'), //TODO: Update with new mechanism 'ttl_default' => intParam('feed_ttl_default'), 'since_hours_posts_per_rss' => intParam('since_hours_posts_per_rss'), 'min_posts_per_rss' => intParam('min_posts_per_rss'), diff --git a/config-user.default.php b/config-user.default.php index 950bef045..5b49d689a 100644 --- a/config-user.default.php +++ b/config-user.default.php @@ -5,8 +5,14 @@ # override. return array ( 'language' => 'en', - 'old_entries' => 3, - 'keep_history_default' => 50, + 'archiving' => [ + 'keep_period' => 'P3M', + 'keep_max' => 200, + 'keep_min' => 50, + 'keep_favourites' => true, + 'keep_labels' => true, + 'keep_unreads' => false, + ], 'ttl_default' => 3600, 'mail_login' => '', 'email_validation_token' => '', diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 01feece52..9235f873a 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -52,6 +52,12 @@ class Minz_Request { } return null; } + public static function paramBoolean($key) { + if (null === $value = self::paramTernary($key)) { + return false; + } + return $value; + } public static function defaultControllerName() { return self::$default_controller_name; } diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 854126b54..2a230e6f8 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -300,7 +300,11 @@ function invalidateHttpCache($username = '') { Minz_Session::_param('touch', uTimeString()); $username = Minz_Session::param('currentUser', '_'); } - return touch(join_path(DATA_PATH, 'users', $username, 'log.txt')); + $ok = @touch(DATA_PATH . '/users/' . $username . '/log.txt'); + if (!$ok) { + //TODO: Display notification error on front-end + } + return $ok; } function listUsers() { diff --git a/p/scripts/category.js b/p/scripts/category.js index c01b1fdd7..c5d36e900 100644 --- a/p/scripts/category.js +++ b/p/scripts/category.js @@ -137,11 +137,34 @@ function init_draggable() { }; } +function archiving() { + const slider = document.getElementById('slider'); + slider.addEventListener('change', function (e) { + if (e.target.id === 'use_default_purge_options') { + slider.querySelectorAll('.archiving').forEach(function (element) { + element.hidden = e.target.checked; + }); + } + }); + slider.addEventListener('click', function (e) { + if (e.target.closest('button[type=reset]')) { + const archiving = document.getElementById('use_default_purge_options'); + if (archiving) { + slider.querySelectorAll('.archiving').forEach(function (element) { + element.hidden = archiving.getAttribute('data-leave-validation') == 1; + }); + } + } + }); +} + if (document.readyState && document.readyState !== 'loading') { init_draggable(); + archiving(); } else if (document.addEventListener) { document.addEventListener('DOMContentLoaded', function () { init_draggable(); + archiving(); }, false); } // @license-end diff --git a/p/scripts/extra.js b/p/scripts/extra.js index bba2e8e2b..1fd8a19de 100644 --- a/p/scripts/extra.js +++ b/p/scripts/extra.js @@ -184,12 +184,32 @@ function init_slider_observers() { }; closer.onclick = function (ev) { - closer.classList.remove('active'); - slider.classList.remove('active'); - return false; + if (data_leave_validation() || confirm(context.i18n.confirmation_default)) { + slider.querySelectorAll('form').forEach(function (f) { f.reset(); }); + closer.classList.remove('active'); + slider.classList.remove('active'); + return true; + } else { + return false; + } }; } +function data_leave_validation() { + const ds = document.querySelectorAll('[data-leave-validation]'); + for (let i = ds.length - 1; i >= 0; i--) { + const input = ds[i]; + if (input.type === 'checkbox' || input.type === 'radio') { + if (input.checked != input.getAttribute('data-leave-validation')) { + return false; + } + } else if (input.value != input.getAttribute('data-leave-validation')) { + return false; + } + } + return true; +} + function init_configuration_alert() { window.onsubmit = function (e) { window.hasSubmit = true; @@ -198,16 +218,8 @@ function init_configuration_alert() { if (window.hasSubmit) { return; } - const ds = document.querySelectorAll('[data-leave-validation]'); - for (let i = ds.length - 1; i >= 0; i--) { - const input = ds[i]; - if (input.type === 'checkbox' || input.type === 'radio') { - if (input.checked != input.getAttribute('data-leave-validation')) { - return false; - } - } else if (input.value != input.getAttribute('data-leave-validation')) { - return false; - } + if (!data_leave_validation()) { + return false; } }; } diff --git a/p/themes/base-theme/template.css b/p/themes/base-theme/template.css index 889d33c4e..2d76c9b4d 100644 --- a/p/themes/base-theme/template.css +++ b/p/themes/base-theme/template.css @@ -101,6 +101,9 @@ label { input { width: 180px; } +input[type=number] { + width: 6em; +} textarea, input[type="file"], diff --git a/phpcs.xml b/phpcs.xml index c69f53ea4..fba5624a8 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -3,8 +3,6 @@ Created with the PHP Coding Standard Generator. https://edorian.github.com/php-coding-standard-generator/ - ./static - ./vendor ./lib/SimplePie/ ./lib/PHPMailer/ ./lib/http-conditional.php @@ -28,9 +26,6 @@ ./app/install.php ./tests/app/ - - ./app/SQL/install.sql.mysql.php - ./app/SQL/install.sql.pgsql.php -- cgit v1.2.3