commit 4b7c97a02ed5fd9807821819302a770dde9ffa2e Author: armansansd Date: Tue Jul 18 10:47:46 2023 +0100 mediatheque diff --git a/config/backups.yaml b/config/backups.yaml new file mode 100755 index 0000000..e69de29 diff --git a/config/groups.yaml b/config/groups.yaml new file mode 100644 index 0000000..75f2f1b --- /dev/null +++ b/config/groups.yaml @@ -0,0 +1,17 @@ +lecteur: + access: + admin: + login: false + super: false + cache: false + configuration: false + pages: false + maintenance: false + statistics: false + plugins: false + themes: false + tools: false + users: false + flex-objects: false + readableName: Lecteur + enabled: true diff --git a/config/media.yaml b/config/media.yaml new file mode 100755 index 0000000..23fbce9 --- /dev/null +++ b/config/media.yaml @@ -0,0 +1,263 @@ +types: + defaults: + type: file + thumb: media/thumb.png + mime: application/octet-stream + image: + filters: + default: [enableProgressive] + jpg: + type: image + thumb: media/thumb-jpg.png + mime: image/jpeg + image: null + jpe: + type: image + thumb: media/thumb-jpg.png + mime: image/jpeg + image: null + jpeg: + type: image + thumb: media/thumb-jpg.png + mime: image/jpeg + image: null + png: + type: image + thumb: media/thumb-png.png + mime: image/png + image: null + gif: + type: animated + thumb: media/thumb-gif.png + mime: image/gif + image: null + svg: + type: vector + thumb: media/thumb-svg.png + mime: image/svg+xml + image: null + mp4: + type: video + thumb: media/thumb-mp4.png + mime: video/mp4 + image: null + mov: + type: video + thumb: media/thumb-mov.png + mime: video/quicktime + image: null + m4v: + type: video + thumb: media/thumb-m4v.png + mime: video/x-m4v + image: null + swf: + type: video + thumb: media/thumb-swf.png + mime: video/x-flv + image: null + flv: + type: video + thumb: media/thumb-flv.png + mime: video/x-flv + image: null + webm: + type: video + thumb: media/thumb-webm.png + mime: video/webm + image: null + ogv: + type: video + thumb: media/thumb-ogg.png + mime: video/ogg + image: null + mp3: + type: audio + thumb: media/thumb-mp3.png + mime: audio/mp3 + image: null + ogg: + type: audio + thumb: media/thumb-ogg.png + mime: audio/ogg + image: null + wma: + type: audio + thumb: media/thumb-wma.png + mime: audio/wma + image: null + m4a: + type: audio + thumb: media/thumb-m4a.png + mime: audio/m4a + image: null + wav: + type: audio + thumb: media/thumb-wav.png + mime: audio/wav + image: null + aiff: + type: audio + thumb: media/thumb-aif.png + mime: audio/aiff + image: null + aif: + type: audio + thumb: media/thumb-aif.png + mime: audio/aif + image: null + txt: + type: file + thumb: media/thumb-txt.png + mime: text/plain + image: null + xml: + type: file + thumb: media/thumb-xml.png + mime: application/xml + image: null + doc: + type: file + thumb: media/thumb-doc.png + mime: application/msword + image: null + docx: + type: file + thumb: media/thumb-docx.png + mime: application/msword + image: null + xls: + type: file + thumb: media/thumb-xls.png + mime: application/vnd.ms-excel + image: null + xlsx: + type: file + thumb: media/thumb-xlsx.png + mime: application/vnd.ms-excel + image: null + ppt: + type: file + thumb: media/thumb-ppt.png + mime: application/vnd.ms-powerpoint + image: null + pptx: + type: file + thumb: media/thumb-pptx.png + mime: application/vnd.ms-powerpoint + image: null + pps: + type: file + thumb: media/thumb-pps.png + mime: application/vnd.ms-powerpoint + image: null + rtf: + type: file + thumb: media/thumb-rtf.png + mime: application/rtf + image: null + bmp: + type: file + thumb: media/thumb-bmp.png + mime: image/bmp + image: null + tiff: + type: file + thumb: media/thumb-tiff.png + mime: image/tiff + image: null + mpeg: + type: file + thumb: media/thumb-mpg.png + mime: video/mpeg + image: null + mpg: + type: file + thumb: media/thumb-mpg.png + mime: video/mpeg + image: null + mpe: + type: file + thumb: media/thumb-mpe.png + mime: video/mpeg + image: null + avi: + type: file + thumb: media/thumb-avi.png + mime: video/msvideo + image: null + wmv: + type: file + thumb: media/thumb-wmv.png + mime: video/x-ms-wmv + image: null + html: + type: file + thumb: media/thumb-html.png + mime: text/html + image: null + htm: + type: file + thumb: media/thumb-html.png + mime: text/html + image: null + ics: + type: iCal + thumb: media/thumb-ics.png + mime: text/calendar + image: null + pdf: + type: file + thumb: media/thumb-pdf.png + mime: application/pdf + image: null + ai: + type: file + thumb: media/thumb-ai.png + mime: image/ai + image: null + psd: + type: file + thumb: media/thumb-psd.png + mime: image/psd + image: null + zip: + type: file + thumb: media/thumb-zip.png + mime: application/zip + image: null + 7z: + type: file + thumb: media/thumb-7z.png + mime: application/x-7z-compressed + image: null + gz: + type: file + thumb: media/thumb-gz.png + mime: application/gzip + image: null + tar: + type: file + thumb: media/thumb-tar.png + mime: application/x-tar + image: null + css: + type: file + thumb: media/thumb-css.png + mime: text/css + image: null + js: + type: file + thumb: media/thumb-js.png + mime: application/javascript + image: null + json: + type: file + thumb: media/thumb-json.png + mime: application/json + image: null + odt: + type: file + thumb: null + mime: null + image: null diff --git a/config/plugins/admin.yaml b/config/plugins/admin.yaml new file mode 100755 index 0000000..c28bff6 --- /dev/null +++ b/config/plugins/admin.yaml @@ -0,0 +1,101 @@ +enabled: true +route: /admin +cache_enabled: true +theme: grav +logo_text: null +body_classes: null +content_padding: true +twofa_enabled: true +sidebar: + activate: tab + hover_delay: 100 + size: auto +dashboard: + days_of_stats: 7 +widgets_display: + dashboard-maintenance: 'true' + dashboard-statistics: 'true' + dashboard-notifications: 'false' + dashboard-feed: 'false' + dashboard-pages: 'true' +pages: + show_parents: both + show_modular: true + parents_levels: null +session: + timeout: 6000 +edit_mode: normal +frontend_preview_target: inline +show_github_msg: true +admin_icons: line-awesome +enable_auto_updates_check: false +notifications: + feed: false + dashboard: false + plugins: false + themes: false +popularity: + enabled: true + ignore: + - '/test*' + - /modular + history: + daily: '30' + monthly: '12' + visitors: '20' +whitelabel: + quicktray_recompile: false + codemirror_theme: paper + codemirror_fontsize: md + codemirror_md_font: sans + logo_custom: { } + logo_login: { } + color_scheme: + accents: + primary-accent: button + secondary-accent: notice + tertiary-accent: critical + colors: + logo-bg: '#323640' + logo-link: '#FFFFFF' + nav-bg: '#3D424E' + nav-text: '#B7B9BD' + nav-link: '#ffffff' + nav-selected-bg: '#323640' + nav-selected-link: '#ffffff' + nav-hover-bg: '#434753' + nav-hover-link: '#ffffff' + toolbar-bg: '#ffffff' + toolbar-text: '#3D424E' + page-bg: '#F6F6F6' + page-text: '#6f7b8a' + page-link: '#0090D9' + content-bg: '#ffffff' + content-text: '#6f7b8a' + content-link: '#0090D9' + content-link2: '#da4b46' + content-header: '#414147' + content-tabs-bg: '#e6e6e6' + content-tabs-text: '#808080' + button-bg: '#0090D9' + button-text: '#ffffff' + notice-bg: '#06A599' + notice-text: '#ffffff' + update-bg: '#77559D' + update-text: '#ffffff' + critical-bg: '#F45857' + critical-text: '#ffffff' + content-highlight: '#ffffd7' + name: null + custom_footer: null + custom_css: null + custom_presets: null +show_beta_msg: null +pagemedia: + resize_width: 0 + resize_height: 0 + res_min_width: 0 + res_min_height: 0 + res_max_width: 0 + res_max_height: 0 + resize_quality: 0.8 diff --git a/config/plugins/auto-author.yaml b/config/plugins/auto-author.yaml new file mode 100755 index 0000000..364d495 --- /dev/null +++ b/config/plugins/auto-author.yaml @@ -0,0 +1,2 @@ +enabled: true +user: true diff --git a/config/plugins/autoseo.yaml b/config/plugins/autoseo.yaml new file mode 100755 index 0000000..d4bbfea --- /dev/null +++ b/config/plugins/autoseo.yaml @@ -0,0 +1,11 @@ +enabled: true +description: + enabled: true + length: 30 +keywords: + enabled: true + length: 20 +facebook: + enabled: true +twitter: + enabled: true diff --git a/config/plugins/cookieconsent.yaml b/config/plugins/cookieconsent.yaml new file mode 100755 index 0000000..3be049a --- /dev/null +++ b/config/plugins/cookieconsent.yaml @@ -0,0 +1,13 @@ +enabled: true +cdn: true +popup_background_color: '#000000' +popup_text_color: '#ffffff' +button_background_color: '#e94d1a' +button_text_color: '#000000' +button_border_color: '#e94d1a' +position: bottom +theme: block +content_message: 'Ce site contient des cookies déposés via des services de partage de vidéos, ils sont destinés à permettre à l''utilisateur de visualiser le contenu multimédia directement sur le site.' +content_dismiss: 'J''accepte' +content_link: 'En savoir plus' +content_href: 'https://policies.google.com/privacy?hl=fr&gl=fr' diff --git a/config/plugins/email.yaml b/config/plugins/email.yaml new file mode 100644 index 0000000..5afc30e --- /dev/null +++ b/config/plugins/email.yaml @@ -0,0 +1,19 @@ +enabled: true +from: contact@armansansd.net +to: contact@armansansd.net +mailer: + engine: sendmail + smtp: + server: localhost + port: 25 + encryption: none + user: null + password: null + sendmail: + bin: '/usr/sbin/sendmail -bs' +content_type: text/html +debug: false +cc: null +bcc: null +reply_to: null +body: null diff --git a/config/plugins/langswitcher.yaml b/config/plugins/langswitcher.yaml new file mode 100755 index 0000000..3066a1a --- /dev/null +++ b/config/plugins/langswitcher.yaml @@ -0,0 +1,3 @@ +enabled: true +built_in_css: true +untranslated_pages_behavior: none diff --git a/config/plugins/pagination.yaml b/config/plugins/pagination.yaml new file mode 100755 index 0000000..c83c903 --- /dev/null +++ b/config/plugins/pagination.yaml @@ -0,0 +1,3 @@ +enabled: true +built_in_css: true +delta: 0 diff --git a/config/plugins/social-seo-metatags.yaml b/config/plugins/social-seo-metatags.yaml new file mode 100755 index 0000000..2da01be --- /dev/null +++ b/config/plugins/social-seo-metatags.yaml @@ -0,0 +1,33 @@ +enabled: true +seo: + robots: without + length: 20 + taxonomy: + enabled: true + page_content: + enabled: false + breadcrumb: false + keywords: + taxonomy: + enabled: true + page_content: + enabled: true + length: 20 +quote: + convert_simple: true +image: + use_cache: true +social_pages: + pages: + twitter: + enabled: true + type: summary_large_image + username: null + facebook: + opengraph: + enabled: false + insights: + enabled: false + appid: '1234567890' +default: + image: { } diff --git a/config/plugins/taxonomylist.yaml b/config/plugins/taxonomylist.yaml new file mode 100755 index 0000000..ca43908 --- /dev/null +++ b/config/plugins/taxonomylist.yaml @@ -0,0 +1,2 @@ +enabled: true +route: /actualites diff --git a/config/plugins/tinymce-editor.yaml b/config/plugins/tinymce-editor.yaml new file mode 100644 index 0000000..0b727c1 --- /dev/null +++ b/config/plugins/tinymce-editor.yaml @@ -0,0 +1,80 @@ +enabled: true +restrictions: + whitelist: false + blacklist: false +whitelist: + items: + - '@root.descendants' + filter: + published: true + non-published: true +blacklist: + items: + - '@none' + filter: + published: true + non-published: true +apikey: r2duxdbywfre1kmk3ft7z998eyt5mnsmkyqcp41hthzk5jcc +plugins: + - advlist + - anchor + - autoresize + - charmap + - code + - colorpicker + - emoticons + - fullscreen + - hr + - image + - insertdatetime + - link + - lists + - media + - nonbreaking + - pagebreak + - paste + - print + - searchreplace + - table + - textcolor + - toc + - visualchars + - wordcount +parameters: + - + name: paste_data_images + value: '1' + - + name: paste_data_images + value: '1' +evals: '' +menubar: false +menu: + - + title: File + items: 'newdocument print' + - + title: Edit + items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' + - + title: Insert + items: 'media link image | pagebreak charmap anchor hr insertdatetime nonbreaking toc' + - + title: View + items: 'visualchars visualaid | fullscreen' + - + title: Format + items: 'bold italic underline strikethrough superscript subscript | formats | removeformat' + - + title: Table + items: 'inserttable tableprops deletetable | cell row column' + - + title: Tools + items: code +toolbar: + - + row: 'formatselect | undo redo | visualchars | image media | link unlink | blockquote | nonbreaking | | bold italic underline | bullist numlist | superscript subscript | removeformat table' + - + row: null +branding: false +statusbar: false diff --git a/config/plugins/tntsearch.yaml b/config/plugins/tntsearch.yaml new file mode 100755 index 0000000..b9a8883 --- /dev/null +++ b/config/plugins/tntsearch.yaml @@ -0,0 +1,28 @@ +enabled: true +search_route: /search +query_route: /s +built_in_css: true +built_in_js: true +built_in_search_page: true +enable_admin_page_events: true +search_type: auto +fuzzy: true +phrases: true +stemmer: default +display_route: true +display_hits: true +display_time: true +live_uri_update: true +limit: '20' +min: '3' +snippet: '300' +index_page_by_default: true +scheduled_index: + enabled: true + at: '0 0 * * 1' + logs: logs/tntsearch-index.out +filter: + items: + - root@.descendants +powered_by: true +search_object_type: Grav diff --git a/config/scheduler.yaml b/config/scheduler.yaml new file mode 100755 index 0000000..e69de29 diff --git a/config/security.yaml b/config/security.yaml new file mode 100755 index 0000000..a89515d --- /dev/null +++ b/config/security.yaml @@ -0,0 +1 @@ +salt: w2FWPbaOYk5Sxg diff --git a/config/site.yaml b/config/site.yaml new file mode 100755 index 0000000..59260cf --- /dev/null +++ b/config/site.yaml @@ -0,0 +1,19 @@ +title: 'Tournons la Page' +default_lang: fr +author: + name: 'tournons la page' + email: contact@tournonslapage.org +taxonomies: + - category + - tag +metadata: + description: 'Tournons La Page est un mouvement citoyen qui entend rassembler le plus largement possible autour d''une vision et des valeurs communes de respect des droits humains fondamentaux et des principes démocratiques.' +summary: + enabled: true + format: short + size: 300 + delimiter: '===' +redirects: null +routes: null +blog: + route: /blog diff --git a/config/streams.yaml b/config/streams.yaml new file mode 100755 index 0000000..e69de29 diff --git a/config/system.yaml b/config/system.yaml new file mode 100644 index 0000000..9dcc868 --- /dev/null +++ b/config/system.yaml @@ -0,0 +1,239 @@ +absolute_urls: false +timezone: null +param_sep: ':' +wrapped_site: false +reverse_proxy_setup: false +force_ssl: false +force_lowercase_urls: true +custom_base_url: null +username_regex: '^[a-z0-9_-]{3,16}$' +pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' +intl_enabled: true +http_x_forwarded: + protocol: true + host: false + port: true + ip: true +languages: + supported: + - fr + - en + default_lang: fr + include_default_lang: true + include_default_lang_file_extension: true + translations: true + translations_fallback: true + session_store_active: false + http_accept_language: false + override_locale: false + pages_fallback_only: false + debug: false +home: + alias: /home + hide_in_urls: false +pages: + type: regular + dirs: + - 'page://' + theme: plain + order: + by: default + dir: asc + list: + count: 20 + dateformat: + default: null + short: 'jS M Y' + long: 'F jS \a\t g:ia' + publish_dates: true + process: + markdown: true + twig: false + twig_first: false + never_cache_twig: false + events: + page: true + twig: true + markdown: + extra: false + auto_line_breaks: false + auto_url_links: false + escape_markup: false + special_chars: + '>': gt + '<': lt + valid_link_attributes: + - rel + - target + - id + - class + - classes + types: + - html + - htm + - xml + - txt + - json + - rss + - atom + append_url_extension: null + expires: 604800 + cache_control: null + last_modified: false + etag: false + vary_accept_encoding: false + redirect_default_code: '302' + redirect_trailing_slash: 1 + redirect_default_route: 0 + ignore_files: + - .DS_Store + ignore_folders: + - .git + - .idea + ignore_hidden: true + hide_empty_folders: false + url_taxonomy_filters: true + frontmatter: + process_twig: false + ignore_fields: + - form + - forms +cache: + enabled: false + check: + method: file + driver: auto + prefix: g + purge_at: '0 4 * * *' + clear_at: '0 3 * * *' + clear_job_type: all + clear_images_by_default: true + cli_compatibility: false + lifetime: 604800 + gzip: true + allow_webserver_gzip: true + redis: + socket: null + password: null + database: null + server: null + port: null + memcache: + server: null + port: null + memcached: + server: null + port: null +twig: + cache: true + debug: false + auto_reload: true + autoescape: false + undefined_functions: true + undefined_filters: true + safe_functions: { } + safe_filters: { } + umask_fix: false +assets: + css_pipeline: true + css_pipeline_include_externals: true + css_pipeline_before_excludes: true + css_minify: true + css_minify_windows: false + css_rewrite: true + js_pipeline: true + js_pipeline_include_externals: true + js_pipeline_before_excludes: true + js_module_pipeline: false + js_module_pipeline_include_externals: true + js_module_pipeline_before_excludes: true + js_minify: true + enable_asset_timestamp: false + enable_asset_sri: false + collections: + jquery: 'system://assets/jquery/jquery-2.x.min.js' +errors: + display: 1 + log: false +log: + handler: file + syslog: + facility: local6 + tag: grav +debugger: + enabled: true + provider: debugbar + censored: false + shutdown: + close_connection: true + twig: true +images: + default_image_quality: 85 + cache_all: false + cache_perms: '0755' + debug: false + auto_fix_orientation: false + seofriendly: false + cls: + auto_sizes: false + aspect_ratio: false + retina_scale: '1' + defaults: + loading: auto + watermark: + image: 'system://images/watermark.png' + position_y: center + position_x: center + scale: 33 + watermark_all: false +media: + enable_media_timestamp: false + unsupported_inline_types: null + allowed_fallback_types: null + auto_metadata_exif: false + upload_limit: 8388608 +session: + enabled: true + initialize: true + timeout: 3000 + name: grav-site + uniqueness: path + secure: false + secure_https: true + httponly: true + samesite: Lax + split: true + domain: null + path: null +gpm: + releases: stable + official_gpm_only: true + method: auto + verify_peer: false +http: + method: auto + enable_proxy: true + proxy_url: null + proxy_cert_path: null + concurrent_connections: 5 + verify_peer: true + verify_host: true +accounts: + type: regular + storage: file + avatar: gravatar +flex: + cache: + index: + enabled: true + lifetime: 60 + object: + enabled: true + lifetime: 600 + render: + enabled: true + lifetime: 600 +strict_mode: + yaml_compat: true + twig_compat: true + blueprint_compat: true diff --git a/config/themes/plain.yaml b/config/themes/plain.yaml new file mode 100755 index 0000000..fdcf423 --- /dev/null +++ b/config/themes/plain.yaml @@ -0,0 +1,3 @@ +enabled: true +dropdown: + enabled: true diff --git a/config/versions.yaml b/config/versions.yaml new file mode 100755 index 0000000..a49eb06 --- /dev/null +++ b/config/versions.yaml @@ -0,0 +1,25 @@ +core: + grav: + version: 1.7.42.1 + history: + version: 1.6.31 + date: '2021-01-15 14:11:30' + 0: { version: 1.7.5, date: '2021-02-08 13:39:58' } + 1: { version: 1.7.7, date: '2021-03-04 13:20:41' } + 2: { version: 1.7.9, date: '2021-03-22 17:05:54' } + 3: { version: 1.7.10, date: '2021-04-07 08:19:14' } + 4: { version: 1.7.15, date: '2021-05-20 08:16:55' } + 5: { version: 1.7.16, date: '2021-06-04 13:32:43' } + 6: { version: 1.7.17, date: '2021-06-21 07:55:11' } + 7: { version: 1.7.18, date: '2021-08-19 07:46:49' } + 8: { version: 1.7.23, date: '2021-10-26 12:14:15' } + 9: { version: 1.7.25, date: '2021-12-11 13:23:27' } + 10: { version: 1.7.26.1, date: '2022-01-13 15:56:19' } + 11: { version: 1.7.27.1, date: '2022-01-13 15:58:17' } + 12: { version: 1.7.30, date: '2022-02-28 13:58:46' } + 13: { version: 1.7.31, date: '2022-03-15 10:23:00' } + 14: { version: 1.7.32, date: '2022-04-22 09:52:03' } + 15: { version: 1.7.33, date: '2022-05-26 10:42:44' } + 16: { version: 1.7.35, date: '2022-08-10 12:01:48' } + 17: { version: 1.7.42.1, date: '2023-06-27 13:17:25' } + schema: 1.7.0_2020-11-20_1 diff --git a/languages/en.yaml b/languages/en.yaml new file mode 100644 index 0000000..d7768ab --- /dev/null +++ b/languages/en.yaml @@ -0,0 +1,12 @@ +DANGER: + ARREST: Arrested since + PRISON: In prison since + FREE: Free since + MENACE: Menaced since +COALITION: Coalitions +MEMBER: Members under threat supported by our emergency fund +ORGA: Organisations +MEDIA: + DOWNLOAD: Download + CONNECTION: Login to access the media + LOGOUT: Log out \ No newline at end of file diff --git a/languages/fr.yaml b/languages/fr.yaml new file mode 100644 index 0000000..b39193a --- /dev/null +++ b/languages/fr.yaml @@ -0,0 +1,12 @@ +DANGER: + ARREST: Arrêté·e le + PRISON: En prison depuis + FREE: Libéré·e le + MENACE: Menacé·e depuis le +COALITION: Coalitions +MEMBER: Membres en danger appuyés par notre fonds d’urgence +ORGA: Organisations +MEDIA: + DOWNLOAD: Télécharger + CONNECTION: Connectez-vous pour accéder au média + LOGOUT: Déconnection \ No newline at end of file diff --git a/plugins/.gitkeep b/plugins/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/plugins/admin-addon-media-metadata/.gitignore b/plugins/admin-addon-media-metadata/.gitignore new file mode 100755 index 0000000..4cc3c5b --- /dev/null +++ b/plugins/admin-addon-media-metadata/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.php_cs.cache diff --git a/plugins/admin-addon-media-metadata/.php_cs b/plugins/admin-addon-media-metadata/.php_cs new file mode 100755 index 0000000..1f79713 --- /dev/null +++ b/plugins/admin-addon-media-metadata/.php_cs @@ -0,0 +1,83 @@ +exclude('vendor/') + ->in(__DIR__); + +// Return a Code Sniffing configuration using +// all sniffers needed for PSR-2 +// and additionally: +// - Remove leading slashes in use clauses. +// - PHP single-line arrays should not have trailing comma. +// - Single-line whitespace before closing semicolon are prohibited. +// - Remove unused use statements in the PHP source code +// - Ensure Concatenation to have at least one whitespace around +// - Remove trailing whitespace at the end of blank lines. +return PhpCsFixer\Config::create() + ->setRiskyAllowed(true) + ->setRules([ + '@PSR2' => true, + 'align_multiline_comment' => [ + 'comment_type' => 'all_multiline', + ], + 'array_syntax' => [ + 'syntax' => 'short' + ], + 'binary_operator_spaces' => [ + 'default' => 'single_space' + ], + 'concat_space' => [ + 'spacing' => 'one' + ], + 'function_typehint_space' => true, + 'hash_to_slash_comment' => true, + 'lowercase_cast' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_consecutive_blank_lines' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_unused_imports' => true, + 'no_unneeded_control_parentheses' => true, + 'no_whitespace_in_blank_line' => true, + 'ordered_imports' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => false, + 'phpdoc_scalar' => true, + 'phpdoc_trim' => true, + 'return_type_declaration' => [ + 'space_before' => 'none' + ], + 'self_accessor' => true, + 'single_quote' => true, + 'standardize_not_equals' => true, + 'whitespace_after_comma_in_array' => true, + ]) + ->setFinder($finder); diff --git a/plugins/admin-addon-media-metadata/CHANGELOG.md b/plugins/admin-addon-media-metadata/CHANGELOG.md new file mode 100755 index 0000000..b3b46e7 --- /dev/null +++ b/plugins/admin-addon-media-metadata/CHANGELOG.md @@ -0,0 +1,37 @@ +# v.1.1.0 +## 05-06-2020 + +1. [](#new) + * Page specific media metadata fields can be added to a page’s frontmatter (see README --> Configuration) +2. [](#improved) + * meta.yaml file will not be created upon upload anymore: this allows the creation of the file with the setting media.auto_metadata_exif turned on (in Admin Plugin 1.9); in Admin Plugin 1.10 it did not work anyway – **NOTE: in Admin Plugin 1.10 with setting media.auto_metadata_exif turned on, make sure to save the page with a newly uploaded image before adding other metadata using this plugin** + +# v1.0.2 +## 16-04-2020 + +1. [](#improved) + * The button next to a media file will now also be translated. It was hard-coded to English before. + +# v1.0.1 +## 10-04-2020 + +1. [](#bugfix) + * meta.yaml file will now be created for an already uploaded media file that did not have an associated meta.yaml file before + +# v1.0.0 +## 20-03-2020 + +1. [](#improved) + * using core technology (Grav\Common\Yaml) for parsing and writing the meta.yaml files – kudos to https://github.com/renards for adding crucial parts to the code + +# v0.9.1 +## 16-03-2020 + +1. [](#bugfix) + * adding /vendor/ and composer.lock, needed for composer autoload functionality + +# v0.9.0 +## 15-03-2020 + +1. [](#new) + * Initial release \ No newline at end of file diff --git a/plugins/admin-addon-media-metadata/LICENSE b/plugins/admin-addon-media-metadata/LICENSE new file mode 100755 index 0000000..3287767 --- /dev/null +++ b/plugins/admin-addon-media-metadata/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Clive Beckett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/admin-addon-media-metadata/README.md b/plugins/admin-addon-media-metadata/README.md new file mode 100755 index 0000000..0838884 --- /dev/null +++ b/plugins/admin-addon-media-metadata/README.md @@ -0,0 +1,90 @@ +# Admin Addon Media Metadata Plugin + +The **Admin Addon Media Metadata** Plugin is an extension for the [Grav CMS](http://github.com/getgrav/grav) [Admin plugin](https://github.com/getgrav/grav-plugin-admin). It lets you add and **edit metadata for media files** in the Page Media browser. + +The Admin plugin has not been offering a feature like this yet. In order to add/edit metadata e.g. for an image you had to create a [image.filename].meta.yaml for the image in your file browser and edit it in a text editor. + +## Usage and Features + +- the plugin will create and edit [mediafile].meta.yaml files in your page folder via a simple form in the Admin plugin +- by default you can add/edit a **title,** **alt** text, and a **caption** – see *Configuration* section below on how to adapt this for your Grav installation +- multiline text can be added (e.g. for caption) +- in case you are storing additional, manually added data in your [mediafile].meta.yaml file, it will not be overwritten even if the form does not let you change it + +### How to use it + +*(see also screenshots below)* + +1. hover any file in your Page Media section +2. hit the small «i» button to open the metadata form *(the regular «i» button which just showed the metadata will be overwritten by the plugin)* + +### NOTE on setting “Auto metadata from Exif” + +If your system is set to automatically write EXIF metadata +`(system.yaml → media.auto_metadata_exif: true)` + +- On **Admin Plugin 1.10:** +Please **save the page** after uploading an image and **before** you write additional metadata using this plugin. On saving the page, the Admin Plugin will create the meta.yaml file including the EXIF data – but it won’t do so if a meta.yaml file already exists. +- On **Admin Plugin 1.9** and **admin-addon-media-metadata >= 1.1.0:** +There’s no problem here: the EXIF data will be written immediately upon upload whereas this plugin now only writes or edits meta.yaml files when needed. + +## Installation + +### Grav Package Manager (GPM) + +If you can access your Grav installation via the command line, install the plugin by typing the following from your Grav root: + +``` +bin/gpm install admin-addon-media-metadata +``` + +### Admin Tool Web Interface + +In the **Plugins** section, hit the **[+ Add]** button, search for Admin Addon Media Metadata and install. + +### Manual Installation + +To install the plugin manually, download the ZIP version of the latest release of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `admin-addon-media-metadata`. You can find these files on [GitHub](https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata/releases). + +## Configuration + +The default fields in the metadata form are alt, title, and caption. If you want to add more data to your meta.yaml files, please copy +`user/plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml` to +`user/config/plugins/admin-addon-media-metadata.yaml` +and add more form fields to the form by updating the copy. E.g. if you want to add a field for a web link, you might add the following lines: + +``` + - type: text + label: Web link + name: weblink + placeholder: https://domain.tld/ +``` + +The URL field will be available in your metadata form and you’ll be able edit the information. + +### Additional Page Specific Metadata Fields (v1.1 and later) + +You may also add page specific fields to a page’s frontmatter. *Note: The fields will be added to the form, not override the ones from the above mentioned config files:* + +``` +admin-addon-media-metadata: + metadata_form: + fields: + - + type: text + label: 'Yet another field' + name: yetAnotherField + placeholder: 'yet yet yet' +``` + +## Credits + +I have based the plugin on Dávid Szabó’s [Admin Addon Media Rename plugin](https://github.com/david-szabo97/grav-plugin-admin-addon-media-rename). Much of the code would not have been possible for me without Dávid’s work. + +[@renards](https://github.com/renards) helped a lot in replacing my original self-written Yaml parsing and writing code with Grav core technology and thus made version 1.0.0 possible. + +## Screenshots + +![](assets/1-open-form.jpg) + +![](assets/2-form-opened.jpg) diff --git a/plugins/admin-addon-media-metadata/admin-addon-media-metadata.css b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.css new file mode 100755 index 0000000..ba13c87 --- /dev/null +++ b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.css @@ -0,0 +1,40 @@ +span[data-dz-name] { + cursor: text; +} + +.dz-metadata-edit { + display: none; + position: absolute; + width: 25px; + height: 25px; + right: -26px; + font-size: 0; + cursor: pointer; + background: #ededed; +/* bottom: 25px; */ + top: 49px; +} + +.dz-metadata-edit:after { + text-align: center; + display: block; + font-family: FontAwesome; + font-size: 18px; + line-height: 25px; +/* content: "\f044"; */ + content: "\f05a"; + color: #737c81; +} + +.dz-metadata-edit:hover:after { + color: #0082ba; +} + +.dropzone .dz-preview:hover .dz-metadata-edit { + display: block; +} + +div.form-label { padding-right: 1rem; } +[data-remodal-id=modal-admin-addon-media-metadata] h1 { line-height: 1; } +form h1 span.header { } +form h1 span.filename { font-size: 1rem; } diff --git a/plugins/admin-addon-media-metadata/admin-addon-media-metadata.js b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.js new file mode 100755 index 0000000..e366125 --- /dev/null +++ b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.js @@ -0,0 +1,113 @@ +/** + * some of the code is based on the Admin Addon Media Rename by Dávid Szabó + * see https://github.com/david-szabo97/grav-plugin-admin-addon-media-rename + */ +$(function() { + + adminAddonMediaMetadata.metadata = {}; + + var clickedElement; + var fileName; + var $elementName; + var modal; + var $modal; + var isPageMedia = false; + + // Append modal + $('body').append(adminAddonMediaMetadata.MODAL); + + $(document).off('click', '[data-dz-name], .dz-metadata-edit'); + $(document).on('click', '[data-dz-name], .dz-metadata-edit', function(e) { + clickedElement = $(this); + modal = $.remodal.lookup[$('[data-remodal-id=modal-admin-addon-media-metadata]').data('remodal')]; + $modal = modal.$modal; + modal.open(); + + // Populate fields + elementName = clickedElement.closest('.dz-preview').find('[data-dz-name]') + fileName = elementName.text(); + $('[name=filename]', $modal).val(fileName); + if (mediaListOnLoad[fileName] !== undefined) { + for (var i = 0; i < metadataFormFields.length; i++) { + $('[name=' + metadataFormFields[i] + ']', $modal).val(mediaListOnLoad[fileName][metadataFormFields[i]]); + } + } else { + for (var i = 0; i < metadataFormFields.length; i++) { + $('[name=' + metadataFormFields[i] + ']', $modal).val(''); + } + } + $modal.find('form span.filename').text(fileName); + + // Reset loading state + $('.loading', $modal).addClass('hidden'); +// $('.button', $modal).removeClass('hidden').css('visibility', 'hidden'); + $('.button', $modal).removeClass('hidden'); + + isPageMedia = !clickedElement.closest('.dz-preview').hasClass('dz-no-editor'); + $modal.find('.block-toggle').toggleClass('hidden', !isPageMedia); + $modal.find('.page-media-info').toggleClass('hidden', !isPageMedia); + $modal.find('.non-page-media-info').toggleClass('hidden', isPageMedia); + }); + + /** + * add the new data dynamically to mediaListOnLoad JSON object + * for newly uploaded media files as well as for “older” files, overwriting the data already in that JSON object + * and save it in the YAML file + * Variables from above ($(document).on), like fileName and $modal are still available + */ + $(document).on('click', '[data-remodal-id=modal-admin-addon-media-metadata] .button', function(e) { + // hiding the button and showing the loading icon + $('.loading', $modal).removeClass('hidden'); + $('.button', $modal).addClass('hidden'); + + // create the file object for newly uploaded files + if (mediaListOnLoad[fileName] === undefined) { + mediaListOnLoad[fileName] = new Object(); + mediaListOnLoad[fileName]['filename'] = fileName; + } + + // Do request with JS Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch + var data = new FormData(); + data.append('filename', fileName); + data.append('admin-nonce', GravAdmin.config.admin_nonce); + + // add the new values to mediaListOnLoad (for later updates before page reload) + // append the new values to the FormData (data.append) + for (var i = 0; i < metadataFormFields.length; i++) { + var newVal = $('[name=' + metadataFormFields[i] + ']', $modal).val(); + mediaListOnLoad[fileName][metadataFormFields[i]] = newVal; + data.append(metadataFormFields[i], newVal); + } + + fetch(adminAddonMediaMetadata.PATH, { method: 'POST', body: data, credentials: 'same-origin' }) + //.then(res => res.json()) + .then(result => { + if (result.error) { + var alertModal = $.remodal.lookup[$('[data-remodal-id=modal-admin-addon-media-metadata-alert]').data('remodal')]; + alertModal.open(); + $('p', alertModal.$modal).html(result.error.msg); + return; + } + + modal.close(); + }); + }); + + // adding the “edit metadata” button next to all media files + setInterval(function addMetadataButton() { + $('.dz-preview').each(function(i, dz) { + if ($(this).find('.dz-metadata-edit').length == 0) { + var editButton = document.createElement('a'); + //editButton.href = 'javascript:undefined;'; + editButton.title = adminAddonMediaMetadataButton; + editButton.className = 'dz-metadata-edit'; + editButton.innerText = adminAddonMediaMetadataButton; + var fileName = $(this).find('[data-dz-name]').text(); + editButton.setAttribute('data-filename', fileName); + $(this).append(editButton); + } + $(this).find('.dz-metadata').remove(); + }); + }, 1000); +}); + diff --git a/plugins/admin-addon-media-metadata/admin-addon-media-metadata.php b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.php new file mode 100755 index 0000000..3ab5a45 --- /dev/null +++ b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.php @@ -0,0 +1,321 @@ +=1.7 + 'onPluginsInitialized' => ['onPluginsInitialized', 0] + ]; + } + + /** + * Composer autoload. + * is + * @return ClassLoader + */ + public function autoload(): ClassLoader + { + return require __DIR__ . '/vendor/autoload.php'; + } + + public function getPath() + { + return '/' . trim($this->grav['admin']->base, '/') . '/' . trim(self::ROUTE, '/'); + } + + public function buildBaseUrl() + { + return rtrim($this->grav['uri']->rootUrl(true), '/') . '/' . trim($this->getPath(), '/'); + } + + /** + * Initialize the plugin + */ + public function onPluginsInitialized() + { + if (!$this->isAdmin() || !$this->grav['user']->authenticated) { + return; + } + + if ($this->grav['uri']->path() == $this->getPath()) { + $this->enable([ + 'onPagesInitialized' => ['processRenameRequest', 0] + ]); + return; + } + + $this->enable([ + 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], + 'onPagesInitialized' => ['onTwigExtensions', 0], + //'onAdminAfterAddMedia' => ['createMetaYaml', 0], // removing the call of this method: see method below + 'onAdminTaskExecute' => ['editMetaDataFile', 0], + ]); + } + + public function onTwigTemplatePaths() + { + $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; + } + + public function onTwigExtensions() + { + $page = $this->grav['admin']->page(true); + if (!$page) { + return; + } + + /** + * get metadata form field config from plugin admin-addon-media-metadata.yaml file + * or local override in user/config/plugins/admin-addon-media-metadata.yaml + * or from page frontmatter + */ + //$formFields = $this->config->get('plugins.admin-addon-media-metadata.metadata_form'); + $config = $this->mergeConfig($page, true); + $formFields = $config->get('metadata_form'); + + /** + * list all needed data keys from the fields configuration + */ + $arrMetaKeys = $this->editableFields($formFields['fields']); + + $path = $page->path(); + $media = new Media($path); + $allMedia = $media->all(); + $arrFiles = []; + $i = 0; + foreach ($allMedia as $filename => $file) { + $metadata = $file->meta(); + $arrFiles[$filename] = [ + 'filename' => $filename, + ]; + /** + * for each file: write stored metadata for each editable field into an array + * this will be output as inline JS variable adminAddonMediaMetadata + */ + foreach ($arrMetaKeys as $metaKey => $info) { + $arrFiles[$filename][$metaKey] = $metadata->$metaKey; + } + $i++; + } + + $jsArrFormFields = ''; + $i = 0; + foreach ($arrMetaKeys as $metaKey => $info) { + $jsArrFormFields .= ($i > 0) ? ",'" . $metaKey . "'" : "'" . $metaKey . "'"; + $i++; + } + + $inlineJs = 'var metadataFormFields = [' . $jsArrFormFields . '];'; + $inlineJs .= PHP_EOL . 'var mediaListOnLoad = ' . json_encode($arrFiles) . ';'; + $modal = $this->grav['twig']->twig()->render('metadata-modal.html.twig', $formFields); + $jsConfig = [ + 'PATH' => $this->buildBaseUrl() . '/' . $page->route() . '/task:' . self::TASK_METADATA, + 'MODAL' => $modal + ]; + $inlineJs .= PHP_EOL . 'var adminAddonMediaMetadata = ' . json_encode($jsConfig) . ';'; + $inlineJs .= PHP_EOL . $this->grav['twig']->twig()->render('additionalInlineJS.html.twig'); + $this->grav['assets']->addInlineJs($inlineJs, -1000); + $this->grav['assets']->addCss('plugin://admin-addon-media-metadata/admin-addon-media-metadata.css', -1000); + $this->grav['assets']->addJs('plugin://admin-addon-media-metadata/admin-addon-media-metadata.js', -1000); + } + + public function editTest() + { + $this->outputError('blob'); + } + + /** + * writes metadata into the [mediafile].meta.yaml file + * creates the .meta.yaml file if it does not yet exist + */ + public function editMetaDataFile($e) + { + $method = $e['method']; + if ($method === 'task' . self::TASK_METADATA) { + $fileName = $_POST['filename']; + + $pageObj = $this->grav['admin']->page(); + $basePath = $pageObj->path() . DS; + + $filePath = $basePath . $fileName; + +/** + * temporarily removing the condition that checks for the media file to exist + * as in Admin plugin 1.10 a media file will be uploaded to a tmp folder first + * and moved to the page folder on saving the page + * + * there needs to be a better solution for this + * + * also: take care of system.yaml → media.auto_metadata_exif + * if set to TRUE, a meta.yaml with EXIF data will be written, but only if the meta.yaml does not yet exist + * in Admin 1.10 you need to save a page in order to have the meta.yaml file with EXIF data created + * in Admin 1.9 this is not a problem since this plugin now (>=1.1.0) writes metadata only when needed + */ +// if (!file_exists($filePath)) { +// $this->outputError($this->grav['language']->translate(['PLUGIN_ADMIN_ADDON_MEDIA_METADATA.ERRORS.MEDIA_FILE_NOT_FOUND', $filePath])); +// } else { + $metaDataFilePath = $filePath . '.meta.yaml'; + + /** + * get the list of form data from the fields configuration + */ + $arrMetaKeys = $this->editableFields(); + + if (file_exists($metaDataFilePath)) { + /** + * get array of all current metadata for the file + * this is to avoid overwriting data that has been added to the meta.yaml file in the file browser + */ + $storedMetaData = Yaml::parse(file_get_contents($metaDataFilePath)); + + /** + * overwrite the currently stored data for each field in the form + */ + foreach ($arrMetaKeys as $metaKey => $info) { + if (isset($_POST[$metaKey])) { + $storedMetaData[$metaKey] = $_POST[$metaKey]; + } + } + + } else { + /** + * create metaData in case the meta data file does not yet exist + */ + $storedMetaData = []; + foreach ($arrMetaKeys as $metaKey => $info) { + if (isset($_POST[$metaKey])) { + $storedMetaData[$metaKey] = $_POST[$metaKey]; + } else { + $storedMetaData[$metaKey] = ''; + } + } + } + + /** + * Get an instance of the meta file and write the data to it + * @see \Grav\Common\Page\Media + */ + $metaDataFile = File::instance($metaDataFilePath); + $metaDataFile->save(Yaml::dump($storedMetaData)); + + //$this->outputError($newYamlText); +// } + } + } + +/** + * this method will not be called in Admin Plugin v1.10 (up until rc.11 anyway) + * additionally, writing the YAML file on upload will meddle with the system functionality + * that writes exif data + * in Admin Plugin v1.9 this functionality writes the meta.yaml file + * on upload, this plugin will then just add the other metadata + * when writing the meta.yaml file upon upload the system functionality will be overwritten + * in Admin Plugin v1.10 you need to save the page before using this plugin on a file + * if you want the exif data to be stored + */ + /** + * creates an image.meta.yaml file + * this file will be deleted by the core when deleting an image + * will be called after a media file has been added (see onPluginsInitialized()) + */ +// public function createMetaYaml() +// { +// $fileName = $_FILES['file']['name']; +// +// $pageObj = $this->grav['admin']->page(); +// $basePath = $pageObj->path() . DS; +// +// $filePath = $basePath . $fileName; +// if (!file_exists($filePath)) { +// $this->outputError($this->grav['language']->translate(['PLUGIN_ADMIN_ADDON_MEDIA_METADATA.ERRORS.MEDIA_FILE_NOT_FOUND', $filePath])); +// } else { +// // TODO: do that only for image files? +// $metaDataFileName = $fileName . '.meta.yaml'; +// $metaDataFilePath = $basePath . $metaDataFileName; +// if (!file_exists($metaDataFilePath)) { +// /** +// * get the list of form data from the fields configuration +// */ +// $arrMetaKeys = $this->editableFields(); +// +// $newMetaData = []; +// foreach ($arrMetaKeys as $metaKey => $info) { +// $newMetaData[$metaKey] = ''; +// } +// +// /** +// * Get an instance of the meta file and write the data to it +// * @see \Grav\Common\Page\Media +// */ +// $metaDataFile = File::instance($metaDataFilePath); +// $metaDataFile->save(Yaml::dump($newMetaData)); +// } +// } +// } + + /** + * return all editable fields from form configuration + */ + private function editableFields($fieldsConf = null) + { + if ($fieldsConf === null) { + $page = $this->grav['admin']->page(true); + if (!$page) { + return; + } + /** + * get metadata form field config from plugin admin-addon-media-metadata.yaml file + * or local override in user/config/plugins/admin-addon-media-metadata.yaml + * or from page frontmatter + */ + //$formFields = $this->config->get('plugins.admin-addon-media-metadata.metadata_form'); + $config = $this->mergeConfig($page, true); + $formFields = $config->get('metadata_form'); + $fieldsConf = $formFields['fields']; + } + $arrMetaKeys = []; + foreach ($fieldsConf as $singleFieldConf) { + if (isset($singleFieldConf['name'], $singleFieldConf['type']) && $singleFieldConf['name'] !== 'filename') { + $arrMetaKeys[$singleFieldConf['name']] = [ + 'name' => $singleFieldConf['name'], + 'type' => $singleFieldConf['type'] + ]; + } + } + return $arrMetaKeys; + } + + public function outputError($msg) + { + header('HTTP/1.1 400 Bad Request'); + die(json_encode(['error' => ['msg' => $msg]])); + } +} diff --git a/plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml new file mode 100755 index 0000000..ae296fe --- /dev/null +++ b/plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml @@ -0,0 +1,21 @@ +enabled: true + +metadata_form: + fields: + - type: hidden + title: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.FILENAME + name: filename + readonly: true + + - type: text + label: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.TITLE + name: title + + - type: text + label: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.ALT + name: alt + + - type: textarea + label: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.CAPTION + name: caption + rows: 3 diff --git a/plugins/admin-addon-media-metadata/assets/1-open-form.jpg b/plugins/admin-addon-media-metadata/assets/1-open-form.jpg new file mode 100755 index 0000000..1daff1a Binary files /dev/null and b/plugins/admin-addon-media-metadata/assets/1-open-form.jpg differ diff --git a/plugins/admin-addon-media-metadata/assets/2-form-opened.jpg b/plugins/admin-addon-media-metadata/assets/2-form-opened.jpg new file mode 100755 index 0000000..a547686 Binary files /dev/null and b/plugins/admin-addon-media-metadata/assets/2-form-opened.jpg differ diff --git a/plugins/admin-addon-media-metadata/blueprints.yaml b/plugins/admin-addon-media-metadata/blueprints.yaml new file mode 100755 index 0000000..14b9eb3 --- /dev/null +++ b/plugins/admin-addon-media-metadata/blueprints.yaml @@ -0,0 +1,30 @@ +name: Admin Addon Media Metadata +version: 1.1.0 +description: 'This plugin is an addon for the Grav CMS Admin plugin and lets you add and edit metadata for media files' +icon: plug +author: + name: Clive Beckett + email: clive@musikinsnetz.de +homepage: https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata +keywords: grav, admin, plugin, metadata, image, alt, title, caption +bugs: https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata/issues +docs: https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata/blob/develop/README.md +license: MIT + +dependencies: + - { name: grav, version: '1.5 - 1.7' } + - { name: admin, version: '1.8 - 1.10' } + +form: + validation: loose + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/admin-addon-media-metadata/composer.json b/plugins/admin-addon-media-metadata/composer.json new file mode 100755 index 0000000..92d3755 --- /dev/null +++ b/plugins/admin-addon-media-metadata/composer.json @@ -0,0 +1,29 @@ +{ + "name": "clivebeckett/admin-addon-media-metadata", + "type": "grav-plugin", + "description": "Add Metadata (title, alt, caption) to image files in admin plugin", + "keywords": ["plugin"], + "homepage": "https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata", + "license": "MIT", + "authors": [ + { + "name": "Clive Beckett", + "email": "clive@musikinsnetz.de", + "role": "Developer" + } + ], + "require": { + "php": ">=7.1.3" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\AdminAddonMediaMetadata\\": "classes/" + }, + "classmap": ["admin-addon-media-metadata.php"] + }, + "config": { + "platform": { + "php": "7.1.3" + } + } +} \ No newline at end of file diff --git a/plugins/admin-addon-media-metadata/composer.lock b/plugins/admin-addon-media-metadata/composer.lock new file mode 100755 index 0000000..7724b63 --- /dev/null +++ b/plugins/admin-addon-media-metadata/composer.lock @@ -0,0 +1,23 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b884fad27830ca91d1b5af86da9aff06", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.1.3" + }, + "platform-dev": [], + "platform-overrides": { + "php": "7.1.3" + }, + "plugin-api-version": "1.1.0" +} diff --git a/plugins/admin-addon-media-metadata/languages/de.yaml b/plugins/admin-addon-media-metadata/languages/de.yaml new file mode 100755 index 0000000..0dfd7c0 --- /dev/null +++ b/plugins/admin-addon-media-metadata/languages/de.yaml @@ -0,0 +1,11 @@ +PLUGIN_ADMIN_ADDON_MEDIA_METADATA: + BUTTON: Metadaten ändern + FORM_HEADER: Metadaten für Mediendatei ändern + FILENAME: Dateiname + TITLE: Titel + ALT: Beschreibung für den Fall, dass die Datei nicht angezeigt werden kann (alternativer Text) + CAPTION: Bildunterschrift + WRITING_DATA: Metadaten werden geschrieben + WRITING_FAILED: Metadaten konnten nicht geschrieben werden + ERRORS: + MEDIA_FILE_NOT_FOUND: Die Mediendatei konnte nicht gefunden werden diff --git a/plugins/admin-addon-media-metadata/languages/en.yaml b/plugins/admin-addon-media-metadata/languages/en.yaml new file mode 100755 index 0000000..ebcc018 --- /dev/null +++ b/plugins/admin-addon-media-metadata/languages/en.yaml @@ -0,0 +1,11 @@ +PLUGIN_ADMIN_ADDON_MEDIA_METADATA: + BUTTON: Edit metadata + FORM_HEADER: Edit metadata for media file + FILENAME: Filename + TITLE: Title + ALT: Describe the media file for when it can’t be displayed (alternative text) + CAPTION: Caption + WRITING_DATA: Writing metadata + WRITING_FAILED: Writing image metadata failed + ERRORS: + MEDIA_FILE_NOT_FOUND: The media file could not be found diff --git a/plugins/admin-addon-media-metadata/languages/fr.yaml b/plugins/admin-addon-media-metadata/languages/fr.yaml new file mode 100755 index 0000000..3483c29 --- /dev/null +++ b/plugins/admin-addon-media-metadata/languages/fr.yaml @@ -0,0 +1,11 @@ +PLUGIN_ADMIN_ADDON_MEDIA_METADATA: + BUTTON: Editer les métadonnées pour ce média + FORM_HEADER: Editer les métadonnées pour ce média + FILENAME: Nom du fichier + TITLE: Titre + ALT: Description du média lorsqu'il ne peut être affiché (texte alternatif) + CAPTION: Légende + WRITING_DATA: Ajout des métadonnées + WRITING_FAILED: Impossible d'ajouter les métadonnées + ERRORS: + MEDIA_FILE_NOT_FOUND: Le fichier du média ne peut être trouvé diff --git a/plugins/admin-addon-media-metadata/templates/additionalInlineJS.html.twig b/plugins/admin-addon-media-metadata/templates/additionalInlineJS.html.twig new file mode 100755 index 0000000..b6a39f0 --- /dev/null +++ b/plugins/admin-addon-media-metadata/templates/additionalInlineJS.html.twig @@ -0,0 +1,2 @@ +var adminAddonMediaMetadataButton = '{{ 'PLUGIN_ADMIN_ADDON_MEDIA_METADATA.BUTTON'|t }}'; +var adminAddonMediaMetadataLang = '{{ grav.user.language|default('en') }}'; \ No newline at end of file diff --git a/plugins/admin-addon-media-metadata/templates/metadata-modal.html.twig b/plugins/admin-addon-media-metadata/templates/metadata-modal.html.twig new file mode 100755 index 0000000..0d71cc3 --- /dev/null +++ b/plugins/admin-addon-media-metadata/templates/metadata-modal.html.twig @@ -0,0 +1,36 @@ +
+
+
+

+ {{ "PLUGIN_ADMIN_ADDON_MEDIA_METADATA.FORM_HEADER"|t }}
+ +

+
+ {% for field in fields %} + {% if field.type %} + {% set value = data.value(field.name) %} +
+ {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %} +
+ {% endif %} + {% endfor %} + +
+
+ {{ "PLUGIN_ADMIN_ADDON_MEDIA_METADATA.WRITING_DATA"|t }} … +
+ + +
+
+
+ +
+
+

{{ "PLUGIN_ADMIN_ADDON_MEDIA_METADATA.WRITING_FAILED"|t }}

+

+
+ +
+
+
\ No newline at end of file diff --git a/plugins/admin-addon-media-metadata/vendor/autoload.php b/plugins/admin-addon-media-metadata/vendor/autoload.php new file mode 100755 index 0000000..fe8182c --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/plugins/admin-addon-media-metadata/vendor/composer/LICENSE b/plugins/admin-addon-media-metadata/vendor/composer/LICENSE new file mode 100755 index 0000000..f27399a --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/admin-addon-media-metadata/vendor/composer/autoload_classmap.php b/plugins/admin-addon-media-metadata/vendor/composer/autoload_classmap.php new file mode 100755 index 0000000..2147962 --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $baseDir . '/admin-addon-media-metadata.php', +); diff --git a/plugins/admin-addon-media-metadata/vendor/composer/autoload_namespaces.php b/plugins/admin-addon-media-metadata/vendor/composer/autoload_namespaces.php new file mode 100755 index 0000000..b7fc012 --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/classes'), +); diff --git a/plugins/admin-addon-media-metadata/vendor/composer/autoload_real.php b/plugins/admin-addon-media-metadata/vendor/composer/autoload_real.php new file mode 100755 index 0000000..de09f1e --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/composer/autoload_real.php @@ -0,0 +1,55 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/plugins/admin-addon-media-metadata/vendor/composer/autoload_static.php b/plugins/admin-addon-media-metadata/vendor/composer/autoload_static.php new file mode 100755 index 0000000..31d4532 --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'Grav\\Plugin\\AdminAddonMediaMetadata\\' => 36, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Grav\\Plugin\\AdminAddonMediaMetadata\\' => + array ( + 0 => __DIR__ . '/../..' . '/classes', + ), + ); + + public static $classMap = array ( + 'Grav\\Plugin\\AdminAddonMediaMetadataPlugin' => __DIR__ . '/../..' . '/admin-addon-media-metadata.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/plugins/admin-addon-media-metadata/vendor/composer/installed.json b/plugins/admin-addon-media-metadata/vendor/composer/installed.json new file mode 100755 index 0000000..fe51488 --- /dev/null +++ b/plugins/admin-addon-media-metadata/vendor/composer/installed.json @@ -0,0 +1 @@ +[] diff --git a/plugins/admin/.editorconfig b/plugins/admin/.editorconfig new file mode 100644 index 0000000..6375a81 --- /dev/null +++ b/plugins/admin/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +# 4 space indentation +[*.php] +indent_size = 4 diff --git a/plugins/admin/.gitattributes b/plugins/admin/.gitattributes new file mode 100644 index 0000000..c5ef731 --- /dev/null +++ b/plugins/admin/.gitattributes @@ -0,0 +1,8 @@ +# Linguist Normalizer +*.yaml linguistic-language=PHP +*.twig linguistic-language=PHP +**/gulpfile.babel.js linguist-vendored +**/webpack.conf.js linguist-vendored +**/js/*.js linguist-vendored +**/js/*.json linguist-vendored +**/css-compiled/*.css linguist-vendored diff --git a/plugins/admin/.github/FUNDING.yml b/plugins/admin/.github/FUNDING.yml new file mode 100644 index 0000000..e84f52b --- /dev/null +++ b/plugins/admin/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: grav +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/plugins/admin/.github/dependabot.yml b/plugins/admin/.github/dependabot.yml new file mode 100644 index 0000000..203f3c8 --- /dev/null +++ b/plugins/admin/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/plugins/admin/.gitignore b/plugins/admin/.gitignore new file mode 100644 index 0000000..ddab4e4 --- /dev/null +++ b/plugins/admin/.gitignore @@ -0,0 +1,17 @@ +themes/grav/.sass-cache +.DS_Store +crowdin.yaml + +# Node Modules +**/node_modules/** +themes/grav/js/admin.js +themes/grav/js/vendor.js +themes/grav/js/*.map +.idea + +tests/_output/* +tests/_support/_generated/* +tests/cache/* +tests/error.log +/crowdin.yaml +.vscode diff --git a/plugins/admin/CHANGELOG.md b/plugins/admin/CHANGELOG.md new file mode 100644 index 0000000..a7e2136 --- /dev/null +++ b/plugins/admin/CHANGELOG.md @@ -0,0 +1,2625 @@ +# v1.10.42 +## 06/14/2023 + +1. [](#new) + * Added a couple of string translations + +# v1.10.41.2 +## 05/11/2023 + +1. [](#improved) + * Fixed an issue with `lastBackup()` that caused admin dashboard to fail with an error. + +# v1.10.41.1 +## 05/09/2023 + +1. [](#improved) + * Fixed another Toolbox deprecation error for `lastBackup()` + +# v1.10.41 +## 05/09/2023 + +1. [](#new) + * Updated to use new `BaconQRCode` version `2.0.8` for new SVG features + PHP 8.2+ fixes +1. [](#improved) + * Require Grav `v1.7.41` + * Fixed a deprecated message where `Admin::$routes` was being dynamically defined + * Fixes to use non-deprecated methods in `ScssCompiler` + +# v1.10.40 +## 03/22/2023 + +1. [](#new) + * Added Github actions for dependabot [#2258](https://github.com/getgrav/grav-plugin-admin/pull/2258) +1. [](#improved) + * Syslog tag fields label added [#2296](https://github.com/getgrav/grav-plugin-admin/pull/2296) + * Updated vendor libraries to the latest versions +1. [](#bugfix) + * Fix more than one file upload [#2317](https://github.com/getgrav/grav-plugin-admin/pull/2317) + +# v1.10.39 +## 02/19/2023 + +1. [](#bugfix) + * Forked and fixed PicoFeed library to support PHP 8.2 + +# v1.10.38 +## 01/02/2023 + +1. [](#new) + * Update copyright dates + * Keep version number in sync with Grav version + +# v1.10.37.1 +## 10/08/2022 + +1. [](#bugfix) + * Removed new GumRoad cart icon + new button styling [getgrav/grav#3631](https://github.com/getgrav/grav/issues/3631) + +# v1.10.37 +## 10/05/2022 + +1. [](#improved) + * Updated vendor libraries to latest versions + * Removed a reference to `SwiftMailer` library to support new **Email** plugin v4.0 + +# v1.10.36 +## 09/08/2022 + +1. [](#bugfix) + * Fixed `fieldset.html.twig` not rendering with `markdown: false` [#2313](https://github.com/getgrav/grav-plugin-admin/pull/2313) + +# v1.10.35 +## 08/04/2022 + +1. [](#improved) + * Improvements in CodeMirror editor in RTL mode [#359](https://github.com/getgrav/grav-plugin-admin/issues/359), [#2297](https://github.com/getgrav/grav-plugin-admin/pull/2297) + +# v1.10.34 +## 06/22/2022 + +1. [](#improved) + * Exposed `UriToMarkdown` util (`Grav.default.Utils.UriToMarkdown`) in admin, to convert links/images +1. [](#bugfix) + * Fixed `Latest Page Updates` permissions [#2294](https://github.com/getgrav/grav-plugin-admin/pull/2294) + +# v1.10.33.1 +## 04/25/2022 + +1. [](#bugfix) + * Reverted [PR#2265](https://github.com/getgrav/grav-plugin-admin/pull/2265) as it broke sections output + +# v1.10.33 +## 04/25/2022 + +1. [](#new) + * Require **Form 6.0.1** +2. [](#improved) + * Added support for a single `field:` vs `fields:` in element form field to store a single value to the option field + * Allow new media collapser logic to configure different cookie storage name location via `data-storage-location` +1. [](#bugfix) + * Fixed nested element form fields + * Fixed `columns` and `column` fields with `.dotted` variables inside to ignore columns and column names + * Fixed initial elements state not being restored + +# v1.10.32 +## 03/28/2022 + +1. [](#new) + * Require **Grav 1.7.32**, **Form 6.0.0**, **Login 3.7.0**, **Email 3.1.6** and **Flex Objects 1.2.0** +2. [](#improved) + * List field: Support for default values other than key/value [#2255](https://github.com/getgrav/grav-plugin-admin/issues/2255) + * Added question icon to admin fields with help text [#2261](https://github.com/getgrav/grav-plugin-admin/issues/2261) +3. [](#bugfix) + * Fix nested `toggleable`: originalValue now checks with `??` instead of `is defined` + +# v1.10.31 +## 03/14/2022 + +1. [](#new) + * Added new local Multiavatar (local generation). **This will be default in Grav 1.8** +2. [](#bugfix) + * Patch `collection.js` [#2235](https://github.com/getgrav/grav-plugin-admin/issues/2235) + +# v1.10.30.2 +## 02/09/2022 + +2. [](#bugfix) + * Fixed regression preventing new `elements` field from saving its state + +# v1.10.30.1 +## 02/09/2022 + +1. [](#improved) + * List field items will now require confirmation before getting deleted + +# v1.10.30 +## 02/07/2022 + +1. [](#new) + * Require **Grav 1.7.30** + * Updated SCSS compiler to v1.10 + * PageMedia can now be collapsed and thumbnails previewed smaller, in order to save room on the page. Selection will be remembered. + * DEPRECATED: Admin field `pages_list_display_field` is no longer available as an option [#2191](https://github.com/getgrav/grav-plugin-admin/issues/2191) + * When listing installable themes/plugins, it is now possible to sort them by [Premium](https://getgrav.org/premium) +2. [](#improved) + * Updated JavaScript dependencies + * Cleaned up JavaScript unused dependencies and warnings + * Removed unused style assets + * Plugins list rows now properly highlight on hover, no more guessing when wanting to disable a plugin! +3. [](#bugfix) + * Fixed `elements` field when it's used inside `list` field + * Fixed issue uploading non-images media when Resolution setting enabled in Admin [#2172](https://github.com/getgrav/grav-plugin-admin/issues/2172) + * Prevent fields from being toggled incorrectly by adding originalValue to childs of fieldset. [#2218](https://github.com/getgrav/grav-plugin-admin/pull/2218) + * Fixed persistent focus on Folder field when Adding page (Safari) [#2209](https://github.com/getgrav/grav-plugin-admin/issues/2209) + * Fixed performance of Plugins / Themes sort in the installation table + * Fixed list field with key/value pairs throwing an exception due to bad value [#2199](https://github.com/getgrav/grav-plugin-admin/issues/2199) + * Fixed disabling/enabling plugin from the list breaking the plugin configuration + +# v1.10.29 +## 01/28/2022 + +1. [](#new) + * Require **Grav 1.7.29** +3. [](#improved) + * Made path handling unicode-safe, use new `Utils::basename()` and `Utils::pathinfo()` everywhere + +# v1.10.28 +## 01/24/2022 + +1. [](#bugfix) + * Clean file names before displaying errors/metadata modals + * Recompiled JS for production [#2225](https://github.com/getgrav/grav-plugin-admin/issues/2225) + +# v1.10.27 +## 01/12/2022 + +1. [](#new) + * Support for `YubiKey OTP` 2-Factor authenticator + * New `elements` container field that shows/hides children fields based on boolean trigger value + * Requires Grav `v1.7.27` and Login `v3.6.2` +2. [](#improved) + * Added new asset language strings + +# v1.10.26.1 +## 01/03/2022 + +3. [](#bugfix) + * Fixed an issue with missing files reference by cached autoloader + +# v1.10.26 +## 01/03/2022 + +2. [](#improved) + * Updated SCSS compiler to v1.9 and other vendor libraries + * Fixed various deprecation warnings + * Localized dialog buttons and icons [#2207](https://github.com/getgrav/grav-plugin-admin/pull/2207) + * Updated copyright year + +# v1.10.25 +## 11/16/2021 + +3. [](#bugfix) + * Fixed unescaped messages in JSON responses + +# v1.10.24 +## 10/26/2021 + +1. [](#new) + * Require **Grav 1.7.24** +2. [](#improved) + * Use new `Http\Response` rather than deprecated `GPM\Response` +3. [](#bugfix) + * Fixed an issue with invalid HTML throwing errors on HTML security scanning + * Clear cache when installing plugins + +# v1.10.23 +## 09/29/2021 + +1. [](#new) + * Updated SCSS compiler to v1.8 +2. [](#improved) + * Updated with latest language strings from Crowdin.com +3. [](#bugfix) + * Fixed images from plugins/themes disappearing when saving twice + +# v1.10.22 +## 09/16/2021 + +1. [](#new) + * Updated SCSS compiler to v1.7 + +# v1.10.21 +## 09/14/2021 + +1. [](#new) + * Require **Grav 1.7.21** +2. [](#improved) + * Added a note about UTC times in scheduler AT syntax help + * Now using a monospaced text-based scheduler AT field in scheduler for simplicity + * Improved `Admin:data()` and `Admin::getConfigurationData()` to be more strict +3. [](#bugfix) + * Fixed configuration save location to point to existing config folder [#2176](https://github.com/getgrav/grav-plugin-admin/issues/2176) + +# v1.10.20 +## 09/01/2021 + +1. [](#bugfix) + * Fixed regression `Argument 4 passed to Grav\Plugin\Form\TwigExtension::prepareFormField() must be of the type array` [#2177](https://github.com/getgrav/grav-plugin-admin/issues/2177) + * Fixed `X-Frame-Options` to be `DENY` in all admin pages to prevent a clickjacking attack + +# v1.10.19 +## 08/31/2021 + +1. [](#new) + * Require **Grav 1.7.19** and **Form 5.1.0** and **Login 3.5.0** + * Updated SCSS compiler to v1.6 +2. [](#improved) + * Updated forms and nested fields to use new form logic + * Admin form now use layout `admin`, meaning you can create admin specific field templates by `forms/fields/myfield/admin-field.html.twig` + * Stop using `|tu` filter, Grav already has the same logic in `|t` for admin + * Remove unneeded escapes + * Allow removal of plugin when disabled [#2167](https://github.com/getgrav/grav-plugin-admin/issues/2167) +3. [](#bugfix) + * Fixed missing values in `fieldset` form field + +# v1.10.18 +## 07/19/2021 + +1. [](#improved) + * Add logic to allow fieldset form field inside a list field [#2159](https://github.com/getgrav/grav-plugin-admin/pull/2159) + +# v1.10.17 +## 06/15/2021 + +1. [](#improved) + * Added timestamp as title in logs date [#2141](https://github.com/getgrav/grav-plugin-admin/issues/2141) + * Use `base64_encode` filter rather than function + * Composer update +1. [](#bugfix) + * Fixed missing `Remove Theme` button when the theme is inactive + * Update taskGetChildTypes() to use Flex Pages (works without the plugin) [#2087](https://github.com/getgrav/grav-plugin-admin/issues/2087) + +# v1.10.16 +## 06/02/2021 + +1. [](#bugfix) + * Fixed issue with some elements overflowing closed list items [#2146](https://github.com/getgrav/grav-plugin-admin/issues/2146) + * Fixed configuration not fully updating on save [#2149](https://github.com/getgrav/grav-plugin-admin/issues/2149) + * Fixed display issue with "+ Add Page" and picking a different route [#2136](https://github.com/getgrav/grav-plugin-admin/issues/2136), [#2145](https://github.com/getgrav/grav-plugin-admin/issues/2145) + * Treat WebP as image when inserting / drag & dropping [#2150](https://github.com/getgrav/grav-plugin-admin/issues/2150) + +# v1.10.15 +## 05/19/2021 + +1. [](#new) + * Updated SCSS compiler to v1.5 +1. [](#improved) + * Updated node modules dev dependencies + * Package.json scripts cleanup + * Recompiled JS for production + * Use `base645_encode` filter rather than function + * Editor: Do not assume images URLs are going to be `http://` (wrong assumption plus not SSL) [#2127](https://github.com/getgrav/grav-plugin-admin/issues/2127) + * Improved Theme Activation + Plugin Enabled logic to ensure configuration is not displayed unless activation/enabled state. Fixes [#2140](https://github.com/getgrav/grav-plugin-admin/issues/2140) +1. [](#bugfix) + * Fixed issue with slugify where single curly quotes in titles would translate to straight single quote [#2101](https://github.com/getgrav/grav-plugin-admin/issues/2101) + * Fix z-index issue with fullscreeen editor (and toolips) [#2143](https://github.com/getgrav/grav-plugin-admin/issues/2143) + +# v1.10.14 +## 04/29/2021 + +1. [](#improved) + * Added a `min_height:` option for list field +1. [](#bugfix) + * Fixed z-index issue for tooltips in sidebar + * Fixed custom files being overridden during theme update [#2135](https://github.com/getgrav/grav-plugin-admin/issues/2135) + +# v1.10.13 +## 04/23/2021 + +1. [](#new) + * Added refresh action button for Folder to ease the regeneration of the slug based on the title. Available also as API entry `Grav.default.Forms.Fields.FolderField.Regenerate()` [#1738](https://github.com/getgrav/grav-plugin-admin/issues/1738) +1. [](#improved) + * Removed sourcemaps references from fork-awesome.min.css [#2122](https://github.com/getgrav/grav-plugin-admin/issues/2122) + * Support native spell checkers in CodeMirror editor [#1266](https://github.com/getgrav/grav-plugin-admin/issues/1266) + * Added new 'Content Highlight' color to presets + * Copying Pages now prompts a dedicated modal that allows for picking title, folder name, parent location, page template and visibility [#1738](https://github.com/getgrav/grav-plugin-admin/issues/1738) + * Updated with latest language translations from Crowdin.com +1. [](#bugfix) + * Moved preset CSS compile to earlier in the process to ensure compilation happens in time. + * Prevent Save actions from Flex Objects to trigger the unsaved unload notice [#2125](https://github.com/getgrav/grav-plugin-admin/issues/2125) + * Fixed audit vulnerabilities in module dependencies and house cleanup [#2096](https://github.com/getgrav/grav-plugin-admin/issues/2096) + * Fixed issue preventing Drag & Drop of media files while in Expert Mode [#1927](https://github.com/getgrav/grav-plugin-admin/issues/1927) + * Fixed broken link colors in `preset.css` which was causing issues with tabs and dropdowns + * Fixed permissions for page related tasks and actions + * Fixed permission check for configuration save [#2130](https://github.com/getgrav/grav-plugin-admin/issues/2130) + * Fixed missing/wrong page categories and tags when multi-language support is enabled [#2107](https://github.com/getgrav/grav-plugin-admin/issues/2107) + +# v1.10.12 +## 04/15/2021 + +1. [](#bugfix) + * Regression: Fixed broken plugin/theme installer in admin + * Fixed error reporting for AJAX tasks if user has no permissions + * Fixed missing slash in password reset URL [#2119](https://github.com/getgrav/grav-plugin-admin/issues/2119) + +# v1.10.11 +## 04/13/2021 + +1. [](#bugfix) + * **IMPORTANT** Fixed security vulnerability that allows installation of plugins with minimal admin privileges [GHSA-wg37-cf5x-55hq](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-wg37-cf5x-55hq) + * Fixed `You have been logged out` message when entering to 2FA authentication due to `/admin/task:getNotifications` AJAX call + * Fixed broken 2FA login when site is not configured to use Flex Users [#2109](https://github.com/getgrav/grav-plugin-admin/issues/2109) + * Fixed error message when user clicks logout link after the session has been expired + +# v1.10.10 +## 04/07/2021 + +1. [](#bugfix) + * Fixed missing `admin-preset.css` in multisite environments + * Regression: Fixed broken 2FA form [#2109](https://github.com/getgrav/grav-plugin-admin/issues/2109) + +# v1.10.9 +## 04/06/2021 + +1. [](#new) + * Requires **Grav 1.7.10** +1. [](#improved) + * Better isolate admin to prevent session related vulnerabilities + * Removed support for custom login redirects for improved security + * Shorten forgot password link lifetime from 7 days to 1 hour + * Updated with latest language translations from Crowdin.com +1. [](#bugfix) + * Fixed issue where Adding a new page and canceling from within Editing would alter the Parent location of the edited page [#2067](https://github.com/getgrav/grav-plugin-admin/issues/2067) + * Fixed and enhanced Range field to be Lists compatible [#2062](https://github.com/getgrav/grav-plugin-admin/issues/2062) + * Fixed ERR_TOO_MANY_REDIRECTS with HTTPS = 'On' [#2100](https://github.com/getgrav/grav-plugin-admin/issues/2100) + * Prevent expert editing mode from anyone else than super users [#2094](https://github.com/getgrav/grav-plugin-admin/issues/2094) + * Fixed login related pages being accessible from admin when user has logged in + * Fixed admin user creation and password reset allowing unsafe passwords + * Fixed missing validation when registering the first admin user + * Fixed reset password email not to have session specific token in it + * Fixed admin controller running before setting `$grav['page']` + +# v1.10.8 +## 03/19/2021 + +1. [](#improved) + * Include alt text and title for images added to the editor [#2098](https://github.com/getgrav/grav-plugin-admin/issues/2098) +1. [](#bugfix) + * Fixed issue replacing `wildcard` field names in flex collections [#2092](https://github.com/getgrav/grav-plugin-admin/pull/2092) + * Fixed legacy Pages having old `modular` reference instead of `module` [#2093](https://github.com/getgrav/grav-plugin-admin/issues/2093) + * Fixed issue where Add New modal would close if selecting an item outside of the modal window. It is now necessary go through the Cancel button and clicking the overlay won't trigger the closing of the modal [#2089](https://github.com/getgrav/grav-plugin-admin/issues/2089), [#2065](https://github.com/getgrav/grav-plugin-admin/issues/2065) + +# v1.10.7 +## 03/17/2021 + +1. [](#improved) + * Force height of Flex pages admin to fit available space + * Updated languages from Crowdin.com + * Better field type definitions for file, pagemedia, filepicker and pagemediafield +1. [](#bugfix) + * Fixed error when checking missing log file [#2088](https://github.com/getgrav/grav-plugin-admin/issues/2088) + +# v1.10.6 +## 02/23/2021 + +1. [](#new) + * Vastly improved support for RTL languages [#2078](https://github.com/getgrav/grav-plugin-admin/pull/2078) +1. [](#improved) + * Flex pages admin better uses available space [#2075](https://github.com/getgrav/grav/issues/2075) +1. [](#bugfix) + * Regression: Fixed enabling/disabling plugin or theme corrupting configuration + * Fixed unnecessary closing bracket causing JS error [#2079](https://github.com/getgrav/grav-plugin-admin/issues/2079) + * Fixed wrong language in Admin Tools [#2077](https://github.com/getgrav/grav-plugin-admin/issues/2077) + +# v1.10.5 +## 02/18/2021 + +1. [](#bugfix) + * Regression: Fixed fatal error in admin if POST request has `data` in it [#2074](https://github.com/getgrav/grav-plugin-admin/issues/2074) + * Fixed Admin creating empty `user/config/info.yaml` file (the file can be safely removed, it is not in use) + * Fixed ACL for users with mixed case usernames [#2073](https://github.com/getgrav/grav-plugin-admin/issues/2073) + +# v1.10.4 +## 02/17/2021 + +1. [](#new) + * Added support to include new page creation modals in other pages by using `form_action` twig variable [#2024](https://github.com/getgrav/grav-plugin-admin/pull/2024) + * Updated all languages from [Crowdin](https://crowdin.com/project/grav-admin) - Please update any translations here +1. [](#improved) + * Removed `noscript` template, because 2021... + * List field: added new `placement` property to decide wether to add new items at the top, bottom or based on the *position* of the clicked button [#2055](https://github.com/getgrav/grav-plugin-admin/pull/2055) + * Ensure admin default CSS styles load **first**, and presets loads **last** + * Tweaked handling of uploaded files [#1429](https://github.com/getgrav/grav-plugin-admin/issues/1429) + * Provide media object and filename in `onAdminAfterDelMedia` event [#1905](https://github.com/getgrav/grav-plugin-admin/pull/1905) +1. [](#bugfix) + * Fixed case-sensitive `accept` in `filepicker` field + * Fixed HTML Entities in titles [#2028](https://github.com/getgrav/grav-plugin-admin/issues/2028) + * Fixed deleting list field options completely, didn't save changes [#2056](https://github.com/getgrav/grav-plugin-admin/issues/2056) + * Fixed `onAdminAfterAddMedia` and `onAdminAfterDelMedia` events always pointing to the home page + * Fixed ACL for Configuration tabs [#771](https://github.com/getgrav/grav-plugin-admin/issues/771) + * Fixed changelog button showing up in Info page even if user cannot access it + * Fixed toggleable checkboxes being unchecked in fieldset columns [#2063](https://github.com/getgrav/grav-plugin-admin/issues/2063) + * Fixed issue with max backups of zero [#2070](https://github.com/getgrav/grav-plugin-admin/issues/2070) + +# v1.10.3 +## 02/01/2021 + +1. [](#new) + * Requires **Grav 1.7.4** (SemVer library moved to Grav) + * Added back special fonts (including Gantry) +2. [](#bugfix) + * Fixed field type `range` not taking into account legitimate `0` values + * Fixed `Call to a member function trackHit() on null` [#2049](https://github.com/getgrav/grav-plugin-admin/issues/2049) + +# v1.10.2 +## 01/21/2021 + +2. [](#bugfix) + * Fixed admin style compilation failing to save CSS if assets folder does not exist + +# v1.10.1 +## 01/20/2021 + +1. [](#improved) + * Added `watch.sh` for compiling SCSS with native sass compiler +2. [](#bugfix) + * Fixed issue with overlapping sidebar when using fullscreen editor [#2022](https://github.com/getgrav/grav-plugin-admin/issues/2022) + +# v1.10.0 +## 01/19/2021 + +1. [](#new) + * Requires **Grav 1.7 and PHP 7.3.6** + * Read about this release in the [Grav 1.7 Released](https://getgrav.org/blog/grav-1.7-released) blog post + * Read the full list of changes in the [Changelog on GitHub](https://github.com/getgrav/grav-plugin-admin/blob/1.10.0/CHANGELOG.md) + * Please read [Grav 1.7 Upgrade Guide](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide) before upgrading! +1. [](#improved) + * Various notifications improvements +1. [](#bugfix) + * Fixed missed highlight on the selected page in Parents field + * Fixed notifications that would not be remembered as hidden + * Fixed taxonomy field not listing existing options in Flex Pages + * Fixed taxonomy field not working outside pages + * Fixed fatal error when moving a page using the old implementation [#2019](https://github.com/getgrav/grav-plugin-admin/issues/2019) + * Fixed evaluating default value in `hidden` field (thanks @NicoHood) + +# v1.10.0-rc.20 +## 12/14/2020 + +1. [](#improved) + * Cookies now explicitly set `SameSite` to `Lax` unless otherwise specified [#1998](https://github.com/getgrav/grav-plugin-admin/issues/1998) + * Exposed **Cookies** class (`Grav.default.Utils.Cookies`) for developers that need it in Admin. +1. [](#bugfix) + * Fixed Plugins references in Themes details page. + * Fixed issue preventing purchase of Themes within Admin and redirecting instead. + * Regression: Values inside Fieldset do not display [#1995](https://github.com/getgrav/grav-plugin-admin/issues/1995) + +# v1.10.0-rc.19 +## 12/02/2020 + +1. [](#improved) + * Just keeping sync with Grav rc.19 + +# v1.10.0-rc.18 +## 12/02/2020 + +1. [](#new) + * Retired "Secure Delete" and "Warn on page delete". You are now always warned and asked to confirm a deletion. +1. [](#improved) + * Auto-link a plugin/theme license in details if it starts with `http` + * Allow to fallback to `docs:` instead of `readme:` + * Forward a `sid` to GPM when downloading a premium package + * Better support for array field key/value when either key or value is stored empty [#1972](https://github.com/getgrav/grav-plugin-admin/issues/1972) + * Remember the open state of the sidebar [#1973](https://github.com/getgrav/grav-plugin-admin/issues/1973) + * Upgraded node dependencies to latest version. Improved speed of JS compilation. + * Added modal to confirm updating Grav as well as cool down counter before enabling Update button [#1257](https://github.com/getgrav/grav-plugin-admin/issues/1257) + * Better handling of offline/intranet mode when the repository index is missing. Faster admin. [#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916) + * Statistics is now Page View Statistics [#1885](https://github.com/getgrav/grav-plugin-admin/issues/1885) + * It is now possible to use regex as values for "Hide page types in Admin" and "Hide modular page types in Admin" settings [#1828](https://github.com/getgrav/grav-plugin-admin/issues/1828) + * Default to `disabled` state for all cron-jobs +1. [](#bugfix) + * Fixed Safari issue with new ACL picker field [#1955](https://github.com/getgrav/grav-plugin-admin/issues/1955) + * Stop propagation of ACL add button in ACL picker [flex-objects#83](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/83) + * Fixed missing special groups `authors` and `defaults` for pages + * Fixed Page Move action and selection highlight in Parents selector modal [flex-objects#80](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/80) + * Fixed folder auto-naming in Add Module [#1937](https://github.com/getgrav/grav-plugin-admin/issues/1937) + * Fixed remodal issue triggering close when selecting a dropdown item ending outside of scope [#1682](https://github.com/getgrav/grav-plugin-admin/issues/1682) + * Reworked how collapsed lists work so the tooltip is not cut off [#1928](https://github.com/getgrav/grav-plugin-admin/issues/1928) + * Fixed KeepAlive issue where too large of a session value would fire the keep alive immediately [#1860](https://github.com/getgrav/grav-plugin-admin/issues/1860) + * Fixed stringable objects breaking the inputs + * Fixed filepicker, pagemediaselect fields with `multiple: true` and `array: true` [#1580](https://github.com/getgrav/grav-plugin-admin/issues/1580) + +# v1.10.0-rc.17 +## 10/07/2020 + +1. [](#new) + * Support premium themes +1. [](#improved) + * Improved some error messages for better readability + * Strip tags from browser title +1. [](#bugfix) + * More multi-site routing fixes + * Fixed issue that would force a page reload when failing to install/update a plugin or theme. + * Fixed proxy/browser caching issues in admin pages + +# v1.10.0-rc.16 +## 09/01/2020 + +1. [](#improved) + * Made all the `onAdmin*` CRUD events to pass `object` (and backwards compatible `page`) to make them easier to use + * Updated vendor libraries including `SCSSPHP` to v1.2 +1. [](#bugfix) + * Fixed issue with File field being used in Theme/Plugins + * Fixed bad redirection after successful admin login in subdirectory multisite [#1487](https://github.com/getgrav/grav-plugin-admin/issues/1487) + +# v1.10.0-rc.15 +## 07/22/2020 + +1. [](#bugfix) + * Disabled the EXIF library for Dropzone for fixing the orientation as it was getting applied twice [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923) + * Forked Dropzone fo fix issue with Resize + Exif orientation [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923) + * Fixed URI encode for the preview of images names + +# v1.10.0-rc.14 +## 07/09/2020 + +1. [](#improved) + * Completely removed old Google font support for upgrade compatibility +1. [](#bugfix) + * Fixed bad `use` reference to `UserObject` + +# v1.10.0-rc.13 +## 07/01/2020 + +1. [](#improved) + * Improved color picker field + * Trim login route for safety + * Composer update to grab latest vendor libs + +# v1.10.0-rc.12 +## 06/08/2020 + +1. [](#new) + * Added ability to set a preferred markdown editor in user profile + * Added new `onAdminListContentEditors` event to add a custom editor to the list of available +1. [](#bugfix) + * Fixed issue deleting file from a plugin's configuration + * Use `Pages::find()` instead of `Pages::dispatch()` as we do not want to redirect out of admin + * Fixed broken `parent` field when using the old pages + * Fixed broken `file` field preview when using streams in the path + +# v1.10.0-rc.11 +## 05/14/2020 + +1. [](#new) + * Major enhancements to "White Label" functionality including ability to export/import presets + * New horizontal scroller for theme presets + * Codemirror Fontsize / Preset / Font preference options +1. [](#improved) + * Fixed lots of styling issues related to "White Label" presets + * Changed out "One Light" theme for new "Firewatch Light" theme + * New scrolling system based on `SimpleBar` + native CSS scrollbar styling + +# v1.10.0-rc.10 +## 04/30/2020 + +1. [](#new) + * Addd new `taskConvertUrls` method for use with 3rd party editors + +# v1.10.0-rc.9 +## 04/27/2020 + +1. [](#new) + * Added new "White Label" functionality to customize admin colors + logos + * Added badge count for children in the Parents field +1. [](#improved) + * Added markdown support to `text` in `section` field +1. [](#bugfix) + * Prevent loading Pages in Parents field if they don't have children + * Fixed custom folder in `mediapicker` field not working with streams + * Fixed language redirect adding extra language prefix in Flex + * Fixed `Invalid input in "Parent"` when saving page in raw mode [#1869](https://github.com/getgrav/grav-plugin-admin/issues/1869) + +# v1.10.0-rc.8 +## 03/19/2020 + +1. [](#new) + * Added `has-children` flag in parent field data response + * Added `RESET` en lang string +1. [](#bugfix) + * Fixed parent field not working with regular pages + +# v1.10.0-rc.7 +## 03/05/2020 + +1. [](#new) + * Enable admin cache by default (for existing sites, check `Plugins > Admin Panel > Enable Admin Caching`) +1. [](#improved) + * Removed old `scss.sh` and `watch.sh` scripts, use `gulp watch-css` + * Added keysOnly parameter to `AdminPlugin::pagesTypes()` and `AdminPlugin::pagesModularTypes()` methods + * Added ignore parameter to `Admin::types()` and `Admin::modularTypes()` methods + * Improved configuration fields for hiding page types in Admin +1. [](#bugfix) + * Fixed minor UI padding in Flex pages [#1825](https://github.com/getgrav/grav-plugin-admin/issues/1825) + * Fixed `column` and `section` fields loosing user entered value when form submit fails + * Fixed `order` field not working with a new Flex Page + +# v1.10.0-rc.6 +## 02/11/2020 + +1. [](#new) + * Pass phpstan level 1 tests + * Updated semver library to v1.5 + * Require flex-objects plugin +1. [](#improved) + * Added some debugging messages (turned off by default) + +# v1.10.0-rc.5 +## 02/03/2020 + +1. [](#new) + * No changes, just keeping things in sync with Grav RC version + +# v1.10.0-rc.4 +## 02/03/2020 + +1. [](#new) + * Added message to dashboard to install Flex Objects plugin if it is missing + * Updated `permissions` field to use new `$grav['permissions']` + * DEPRECATED `onAdminRegisterPermissions` event, use `PermissionsRegisterEvent::class` event instead + * DEPRECATED `Admin::setPermissions()` and `Admin::addPermissions()`, use `PermissionsRegisterEvent::class` event instead + * DEPRECATED `Admin::getPermissions()`, use `$grav['permissions']->getInstances()` instead +1. [](#improved) + * Added `field.show_label` and `field.label` display logic from frontend forms +1. [](#bugfix) + * Fixed user profile when using `Flex Users` only in admin + * Fixed saving data with empty field, default value (from config, plugin, theme) was used instead + * Fixed JS bug is using empty Grav URI param key + * Fixed bug in toggleable field being disabled with empty value (`''` `0`, `false`, `[]`...) + * Fixed `admin_route()` twig function to work properly with Grav 1.7.0-rc.4, which fixes `Route` base + * Fixed misleading 'Show sensitive data' configuration option wording [#1818](https://github.com/getgrav/grav-plugin-admin/issues/1818) + +# v1.10.0-rc.3 +## 01/02/2020 + +1. [](#new) + * Added ability to display **Changelogs** for `Grav`, `Plugins` and `Themes` + * Added raw root page support for `Flex Pages` + +# v1.10.0-rc.2 +## 12/04/2019 + +1. [](#new) + * Added support for hiding parts of admin by `Deny` permissions (`Flex Users` only) + * Optimized `parent` field for Flex Pages +1. [](#improved) + * Improved `permissions` field to add support for displaying calculated permissions + * Grav 1.7: Updated deprecated `$page->modular()` method calls to `$page->isModule()` + * Output the current process user name in Scheduler instructions + * Translations: rename MODULAR to MODULE everywhere +1. [](#bugfix) + * Fixed `permissions` field with nested permissions + * Fixed Save Shortcut (CTRL + S / CMD + S) not working with new Flex Pages [#1787](https://github.com/getgrav/grav-plugin-admin/issues/1787) + +# v1.10.0-rc.1 +## 11/06/2019 + +1. [](#new) + * Added a new `onAdminLogFiles()` event for 3rd party plugins to register log files for log viewer [#1765](https://github.com/getgrav/grav-plugin-admin/issues/1765) +1. [](#improved) + * Improved delete button UI [#1769](https://github.com/getgrav/grav-plugin-admin/issues/1769) + * Ability to configure display of 3rd party dashboard widgets [#1766](https://github.com/getgrav/grav-plugin-admin/issues/1766) +1. [](#bugfix) + * Fixed administrator user creation when `Flex Users` is enabled + * Fixed minor button alignment in FF [#1760](https://github.com/getgrav/grav-plugin-admin/issues/1760) + +# v1.10.0-beta.10 +## 10/03/2019 + +1. [](#bugfix) + * Regression: Fixed language assignments for the pages without set language + +# v1.10.0-beta.9 +## 09/26/2019 + +1. [](#bugfix) + * Make pages field to work with Flex Pages + +# v1.10.0-beta.8 +## 09/19/2019 + +1. [](#new) + * Add ability to Sanitize SVGs on file upload + * Add ability to Sanitize SVGs in Page media +1. [](#improved) + * YAML linter report now supports multi-language + * Better colors/placement of toolbar buttons in page edit view +1. [](#bugfix) + * Fixed missing language for AJAX requests + * Fixed redirect with absolute language URL + * Fixed issue with user avatar reference not being deleted when image removed + +# v1.10.0-beta.7 +## 08/30/2019 + +1. [](#bugfix) + * Fixed regression: Do not require Flex Objects plugin [grav#2653](https://github.com/getgrav/grav/issues/2653) + +# v1.10.0-beta.6 +## 08/29/2019 + +1. [](#improved) + * Optimized admin for speed (only load frontend pages on demand) + * Updated navigation menu to be fully controlled and overrideable by `onAdminMenu` event + * Lots of Flex Page speed improvements + +# v1.10.0-beta.5 +## 08/11/2019 + +1. [](#new) + * Added `data()` twig function to create data object from an array +1. [](#improved) + * Better support for `array` field into `list` field + * Made RAW blueprints (expert mode) to work properly with Flex Form + * Better support for `clockwork` logs +1. [](#bugfix) + * Fixed issue with nested `list` fields both utilizing the custom `key` functionality + * Regression: Page Preview not working, bad url [#1715](https://github.com/getgrav/grav-plugin-admin/issues/1715) + * Fixed '+New Folder' to work with new parent picker + * Fixed missing XSS check field when editing modular page as raw + * Fixed minor CSS layout issue [#1717](https://github.com/getgrav/grav-plugin-admin/issues/1717) + +# v1.10.0-beta.4 +## 07/01/2019 + +1. [](#new) + * Added `Admin::redirect()` method to allow redirects to be used outside of controllers + * Added `$admin->adminRoute()` method and `admin_route()` twig function to create language aware admin page links + * Renamed `Admin::route()` to `Admin::getCurrentRoute()` and deprecated the old call +1. [](#improved) + * Much improved multi-language support for pages + * Admin redirects should now work better with multiple languages enabled +1. [](#bugfix) + * Fixed default language being renamed to `page.en.md` (English) instead of keeping existing `page.md` filename + * Fixed possibility to override already existing translation by `Save As Language` + * Fixed missing default translation if page used plain `.md` file extension without language code + * Fixed wrong translation showing up as page fallback language + * Integrated Admin 1.9.8 bug fixes + +# v1.10.0-beta.3 +## 06/24/2019 + +1. [](#improved) + * Smarter handling of symlinks in parent field +1. [](#bugfix) + * Fixed issue with windows paths in `parent` field [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699) + +# v1.10.0-beta.2 +## 06/21/2019 + +1. [](#improved) + * Moved Remodal in-house and added support for stackable modals [#1698](https://github.com/getgrav/grav-plugin-admin/issues/1698), [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699) +1. [](#bugfix) + * Fixed missing check for maximum allowed files in `files` field + +# v1.10.0-beta.1 +## 06/14/2019 + +1. [](#new) + * New Parent/Move field using Ajax for better performance + * Improvements to cache clearing when admin cache is enabled + * Require Grav v1.7 + * Use PSR-4 for plugin classes + * Added support for Twig 2.11 (compatible with Twig 1.40+) +1. [](#improved) + * Various admin performance improvements +1. [](#bugfix) + * Fixed admin caching issues + +# v1.9.19 +## 12/14/2020 + +1. [](#bugfix) + * Fixed `pages` field escaping issues, needs Grav update, too [#1990](https://github.com/getgrav/grav-plugin-admin/issues/1990) + * Fixed Plugins references in Themes details page. + * Fixed issue preventing purchase of Themes within Admin and redirecting instead. + * Fixed Page Picker not passing admin token + +# v1.9.18 +## 12/02/2020 + +1. [](#new) + * Never allow Admin pages to be rendered in ``, `'); + return true; + } + + return false; + } + + /** + * Generate HTML from item URL + * + * @access public + * @param Item $item + * @return bool + */ + public function generateHtmlFromUrl(Item $item) + { + if (preg_match('/youtube\.com\/watch\?v=(.*)/', $item->getUrl(), $matches)) { + $item->setContent(''); + return true; + } + + return false; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php new file mode 100644 index 0000000..caec463 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php @@ -0,0 +1,114 @@ +format('Y-m-d H:i:s').'] '.$message; + } + } + + /** + * Get all logged messages. + * + * @static + * + * @return array + */ + public static function getMessages() + { + return self::$messages; + } + + /** + * Remove all logged messages. + * + * @static + */ + public static function deleteMessages() + { + self::$messages = array(); + } + + /** + * Set a different timezone. + * + * @static + * + * @see http://php.net/manual/en/timezones.php + * + * @param string $timezone Timezone + */ + public static function setTimeZone($timezone) + { + self::$timezone = $timezone ?: self::$timezone; + } + + /** + * Get all messages serialized into a string. + * + * @static + * + * @return string + */ + public static function toString() + { + return implode(PHP_EOL, self::$messages).PHP_EOL; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php new file mode 100644 index 0000000..0496869 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php @@ -0,0 +1,395 @@ + 'http://www.w3.org/2005/Atom', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement[] + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'atom:entry', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'entry'); + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl($this->getUrl($xml, 'self')); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setSiteUrl($this->getUrl($xml, 'alternate', true)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $description = XmlParser::getXPathResult($xml, 'atom:subtitle', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'subtitle'); + + $feed->setDescription(XmlParser::getValue($description)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $logo = XmlParser::getXPathResult($xml, 'atom:logo', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'logo'); + + $feed->setLogo(XmlParser::getValue($logo)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $icon = XmlParser::getXPathResult($xml, 'atom:icon', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'icon'); + + $feed->setIcon(XmlParser::getValue($icon)); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'atom:title', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'title'); + + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $language = XmlParser::getXPathResult($xml, '*[not(self::atom:entry)]/@xml:lang', $this->namespaces) + ?: XmlParser::getXPathResult($xml, '@xml:lang'); + + $feed->setLanguage(XmlParser::getValue($language)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $id = XmlParser::getXPathResult($xml, 'atom:id', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'id'); + + $feed->setId(XmlParser::getValue($id)); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $updated = XmlParser::getXPathResult($xml, 'atom:updated', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'updated'); + + $feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($updated))); + } + + /** + * Find the item published date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'atom:published', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'published'); + + $item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime((string) current($date)) : null); + } + + /** + * Find the item updated date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'atom:updated', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'updated'); + + $item->setUpdatedDate(!empty($date) ? $this->getDateParser()->getDateTime((string) current($date)) : null); + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $title = XmlParser::getXPathResult($entry, 'atom:title', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'title'); + + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $author = XmlParser::getXPathResult($entry, 'atom:author/atom:name', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author/name') + ?: XmlParser::getXPathResult($xml, 'atom:author/atom:name', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'author/name'); + + $item->setAuthor(XmlParser::getValue($author)); + } + + /** + * Find the item author URL. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $authorUrl = XmlParser::getXPathResult($entry, 'atom:author/atom:uri', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author/uri') + ?: XmlParser::getXPathResult($xml, 'atom:author/atom:uri', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'author/uri'); + + $item->setAuthorUrl(XmlParser::getValue($authorUrl)); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $item->setContent($this->getContent($entry)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $item->setUrl($this->getUrl($entry, 'alternate', true)); + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $id = XmlParser::getXPathResult($entry, 'atom:id', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'id'); + + if (!empty($id)) { + $item->setId($this->generateId(XmlParser::getValue($id))); + } else { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $enclosure = $this->findLink($entry, 'enclosure'); + + if ($enclosure) { + $item->setEnclosureUrl(Url::resolve((string) $enclosure['href'], $feed->getSiteUrl())); + $item->setEnclosureType((string) $enclosure['type']); + } + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, './/@xml:lang'); + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Find the item categories. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param Feed $feed Feed object + */ + public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $categories = XmlParser::getXPathResult($entry, 'atom:category/@term', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'category/@term'); + $item->setCategoriesFromXml($categories); + } + + /** + * Get the URL from a link tag. + * + * @param SimpleXMLElement $xml XML tag + * @param string $rel Link relationship: alternate, enclosure, related, self, via + * @return string + */ + private function getUrl(SimpleXMLElement $xml, $rel, $fallback = false) + { + $link = $this->findLink($xml, $rel); + + if ($link) { + return (string) $link['href']; + } + + if ($fallback) { + $link = $this->findLink($xml, ''); + return $link ? (string) $link['href'] : ''; + } + + return ''; + } + + /** + * Get a link tag that match a relationship. + * + * @param SimpleXMLElement $xml XML tag + * @param string $rel Link relationship: alternate, enclosure, related, self, via + * @return SimpleXMLElement|null + */ + private function findLink(SimpleXMLElement $xml, $rel) + { + $links = XmlParser::getXPathResult($xml, 'atom:link', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'link'); + + foreach ($links as $link) { + if ($rel === (string) $link['rel']) { + return $link; + } + } + + return null; + } + + /** + * Get the entry content. + * + * @param SimpleXMLElement $entry XML Entry + * @return string + */ + private function getContent(SimpleXMLElement $entry) + { + $content = current( + XmlParser::getXPathResult($entry, 'atom:content', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'content') + ); + + if (!empty($content) && count($content->children())) { + $xml_string = ''; + + foreach ($content->children() as $child) { + $xml_string .= $child->asXML(); + } + + return $xml_string; + } elseif (trim((string) $content) !== '') { + return (string) $content; + } + + $summary = XmlParser::getXPathResult($entry, 'atom:summary', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'summary'); + + return (string) current($summary); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php new file mode 100644 index 0000000..b7a015d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php @@ -0,0 +1,128 @@ + length ]. + * + * @var array + */ + public $formats = array( + DATE_ATOM => null, + DATE_RSS => null, + DATE_COOKIE => null, + DATE_ISO8601 => null, + DATE_RFC822 => null, + DATE_RFC850 => null, + DATE_RFC1036 => null, + DATE_RFC1123 => null, + DATE_RFC2822 => null, + DATE_RFC3339 => null, + 'l, d M Y H:i:s' => null, + 'D, d M Y H:i:s' => 25, + 'D, d M Y h:i:s' => 25, + 'D M d Y H:i:s' => 24, + 'j M Y H:i:s' => 20, + 'Y-m-d H:i:s' => 19, + 'Y-m-d\TH:i:s' => 19, + 'd/m/Y H:i:s' => 19, + 'D, d M Y' => 16, + 'Y-m-d' => 10, + 'd-m-Y' => 10, + 'm-d-Y' => 10, + 'd.m.Y' => 10, + 'm.d.Y' => 10, + 'd/m/Y' => 10, + 'm/d/Y' => 10, + ); + + /** + * Try to parse all date format for broken feeds. + * + * @param string $value Original date format + * + * @return DateTime + */ + public function getDateTime($value) + { + $value = trim($value); + + foreach ($this->formats as $format => $length) { + $truncated_value = $value; + if ($length !== null) { + $truncated_value = substr($truncated_value, 0, $length); + } + + $date = $this->getValidDate($format, $truncated_value); + if ($date !== false) { + return $date; + } + } + + return $this->getCurrentDateTime(); + } + + /** + * Get a valid date from a given format. + * + * @param string $format Date format + * @param string $value Original date value + * + * @return DateTime|bool + */ + public function getValidDate($format, $value) + { + $date = DateTime::createFromFormat($format, $value, $this->getTimeZone()); + + if ($date !== false) { + $errors = DateTime::getLastErrors(); + + if ($errors === false || ($errors['error_count'] === 0 && $errors['warning_count'] === 0)) { + return $date; + } + } + + return false; + } + + /** + * Get the current datetime. + * + * @return DateTime + */ + public function getCurrentDateTime() + { + return new DateTime('now', $this->getTimeZone()); + } + + /** + * Get DateTimeZone instance + * + * @access public + * @return DateTimeZone + */ + public function getTimeZone() + { + return new DateTimeZone($this->config->getTimezone() ?: $this->timezone); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php new file mode 100644 index 0000000..a56e71c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php @@ -0,0 +1,315 @@ +$property.PHP_EOL; + } + + $output .= 'Feed::date = '.$this->date->format(DATE_RFC822).PHP_EOL; + $output .= 'Feed::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL; + $output .= 'Feed::items = '.count($this->items).' items'.PHP_EOL; + + foreach ($this->items as $item) { + $output .= '----'.PHP_EOL; + $output .= $item; + } + + return $output; + } + + /** + * Get title. + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get description. + */ + public function getDescription() + { + return $this->description; + } + + /** + * Get the logo url. + */ + public function getLogo() + { + return $this->logo; + } + + /** + * Get the icon url. + */ + public function getIcon() + { + return $this->icon; + } + + /** + * Get feed url. + */ + public function getFeedUrl() + { + return $this->feedUrl; + } + + /** + * Get site url. + */ + public function getSiteUrl() + { + return $this->siteUrl; + } + + /** + * Get date. + */ + public function getDate() + { + return $this->date; + } + + /** + * Get language. + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get id. + */ + public function getId() + { + return $this->id; + } + + /** + * Get feed items. + */ + public function getItems() + { + return $this->items; + } + + /** + * Return true if the feed is "Right to Left". + * + * @return bool + */ + public function isRTL() + { + return Parser::isLanguageRTL($this->language); + } + + /** + * Set feed items. + * + * @param Item[] $items + * @return Feed + */ + public function setItems(array $items) + { + $this->items = $items; + return $this; + } + + /** + * Set feed id. + * + * @param string $id + * @return Feed + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set feed title. + * + * @param string $title + * @return Feed + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Set feed description. + * + * @param string $description + * @return Feed + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Set feed url. + * + * @param string $feedUrl + * @return Feed + */ + public function setFeedUrl($feedUrl) + { + $this->feedUrl = $feedUrl; + return $this; + } + + /** + * Set feed website url. + * + * @param string $siteUrl + * @return Feed + */ + public function setSiteUrl($siteUrl) + { + $this->siteUrl = $siteUrl; + return $this; + } + + /** + * Set feed date. + * + * @param \DateTime $date + * @return Feed + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * Set feed language. + * + * @param string $language + * @return Feed + */ + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + /** + * Set feed logo. + * + * @param string $logo + * @return Feed + */ + public function setLogo($logo) + { + $this->logo = $logo; + return $this; + } + + /** + * Set feed icon. + * + * @param string $icon + * @return Feed + */ + public function setIcon($icon) + { + $this->icon = $icon; + return $this; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php new file mode 100644 index 0000000..98214b8 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php @@ -0,0 +1,562 @@ +namespaces); + } + + /** + * Get specific XML tag or attribute value. + * + * @param string $tag Tag name (examples: guid, media:content) + * @param string $attribute Tag attribute + * + * @return array|false Tag values or error + */ + public function getTag($tag, $attribute = '') + { + if ($attribute !== '') { + $attribute = '/@'.$attribute; + } + + $query = './/'.$tag.$attribute; + $elements = XmlParser::getXPathResult($this->xml, $query, $this->namespaces); + + if ($elements === false) { // xPath error + return false; + } + + return array_map(function ($element) { return (string) $element;}, $elements); + } + + /** + * Return item information. + * + * @return string + */ + public function __toString() + { + $output = ''; + + foreach (array('id', 'title', 'url', 'language', 'author', 'enclosureUrl', 'enclosureType') as $property) { + $output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL; + } + + $publishedDate = $this->publishedDate != null ? $this->publishedDate->format(DATE_RFC822) : null; + $updatedDate = $this->updatedDate != null ? $this->updatedDate->format(DATE_RFC822) : null; + + $categoryString = $this->categories != null ? implode(',', $this->categories) : null; + + $output .= 'Item::date = '.$this->date->format(DATE_RFC822).PHP_EOL; + $output .= 'Item::publishedDate = '.$publishedDate.PHP_EOL; + $output .= 'Item::updatedDate = '.$updatedDate.PHP_EOL; + $output .= 'Item::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL; + $output .= 'Item::categories = ['.$categoryString.']'.PHP_EOL; + $output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL; + + return $output; + } + + /** + * Get title. + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get URL + * + * @access public + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL + * + * @access public + * @param string $url + * @return Item + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * Get id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Get date. + * + * @return \DateTime + */ + public function getDate() + { + return $this->date; + } + + /** + * Get published date. + * + * @return \DateTime + */ + public function getPublishedDate() + { + return $this->publishedDate; + } + + /** + * Get updated date. + * + * @return \DateTime + */ + public function getUpdatedDate() + { + return $this->updatedDate; + } + + /** + * Get content. + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Set content + * + * @access public + * @param string $value + * @return Item + */ + public function setContent($value) + { + $this->content = $value; + return $this; + } + + /** + * Get enclosure url. + * + * @return string + */ + public function getEnclosureUrl() + { + return $this->enclosureUrl; + } + + /** + * Get enclosure type. + * + * @return string + */ + public function getEnclosureType() + { + return $this->enclosureType; + } + + /** + * Get language. + * + * @return string + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get categories. + * + * @return array + */ + public function getCategories() + { + return $this->categories; + } + + /** + * Get author. + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Get author URL. + * + * @return string + */ + public function getAuthorUrl() + { + return $this->authorUrl; + } + + /** + * Return true if the item is "Right to Left". + * + * @return bool + */ + public function isRTL() + { + return Parser::isLanguageRTL($this->language); + } + + /** + * Set item id. + * + * @param string $id + * @return Item + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set item title. + * + * @param string $title + * @return Item + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Set author. + * + * @param string $author + * @return Item + */ + public function setAuthor($author) + { + $this->author = $author; + return $this; + } + + /** + * Set author URL. + * + * @param string $authorUrl + * @return Item + */ + public function setAuthorUrl($authorUrl) + { + $this->authorUrl = $authorUrl; + return $this; + } + + /** + * Set item date. + * + * @param \DateTime $date + * @return Item + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * Set item published date. + * + * @param \DateTime $publishedDate + * @return Item + */ + public function setPublishedDate($publishedDate) + { + $this->publishedDate = $publishedDate; + return $this; + } + + /** + * Set item updated date. + * + * @param \DateTime $updatedDate + * @return Item + */ + public function setUpdatedDate($updatedDate) + { + $this->updatedDate = $updatedDate; + return $this; + } + + /** + * Set enclosure url. + * + * @param string $enclosureUrl + * @return Item + */ + public function setEnclosureUrl($enclosureUrl) + { + $this->enclosureUrl = $enclosureUrl; + return $this; + } + + /** + * Set enclosure type. + * + * @param string $enclosureType + * @return Item + */ + public function setEnclosureType($enclosureType) + { + $this->enclosureType = $enclosureType; + return $this; + } + + /** + * Set item language. + * + * @param string $language + * @return Item + */ + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + /** + * Set item categories. + * + * @param array $categories + * @return Item + */ + public function setCategories($categories) + { + $this->categories = $categories; + return $this; + } + + /** + * Set item categories from xml. + * + * @param |SimpleXMLElement[] $categories + * @return Item + */ + public function setCategoriesFromXml($categories) + { + if ($categories !== false) { + $this->setCategories( + array_map( + function ($element) { + return trim((string) $element); + }, + $categories + ) + ); + } + + return $this; + } + + /** + * Set raw XML. + * + * @param \SimpleXMLElement $xml + * @return Item + */ + public function setXml($xml) + { + $this->xml = $xml; + return $this; + } + + /** + * Get raw XML. + * + * @return \SimpleXMLElement + */ + public function getXml() + { + return $this->xml; + } + + /** + * Set XML namespaces. + * + * @param array $namespaces + * @return Item + */ + public function setNamespaces($namespaces) + { + $this->namespaces = $namespaces; + return $this; + } + + /** + * Get XML namespaces. + * + * @return array + */ + public function getNamespaces() + { + return $this->namespaces; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php new file mode 100644 index 0000000..efaf0ff --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php @@ -0,0 +1,13 @@ +fallback_url = $fallback_url; + $xml_encoding = XmlParser::getEncodingFromXmlTag($content); + + // Strip XML tag to avoid multiple encoding/decoding in the next XML processing + $this->content = Filter::stripXmlTag($content); + + // Encode everything in UTF-8 + Logger::setMessage(get_called_class().': HTTP Encoding "'.$http_encoding.'" ; XML Encoding "'.$xml_encoding.'"'); + $this->content = Encoding::convert($this->content, $xml_encoding ?: $http_encoding); + + $this->itemPostProcessor = new ItemPostProcessor($this->config); + $this->itemPostProcessor->register(new ContentGeneratorProcessor($this->config)); + $this->itemPostProcessor->register(new ContentFilterProcessor($this->config)); + } + + /** + * Parse the document. + * @return Feed + * @throws MalformedXmlException + */ + public function execute() + { + Logger::setMessage(get_called_class().': begin parsing'); + + $xml = XmlParser::getSimpleXml($this->content); + + if ($xml === false) { + Logger::setMessage(get_called_class().': Applying XML workarounds'); + $this->content = Filter::normalizeData($this->content); + $xml = XmlParser::getSimpleXml($this->content); + + if ($xml === false) { + Logger::setMessage(get_called_class().': XML parsing error'); + Logger::setMessage(XmlParser::getErrors()); + throw new MalformedXmlException('XML parsing error'); + } + } + + $this->used_namespaces = $xml->getNamespaces(true); + $xml = $this->registerSupportedNamespaces($xml); + + $feed = new Feed(); + + $this->findFeedUrl($xml, $feed); + $this->checkFeedUrl($feed); + + $this->findSiteUrl($xml, $feed); + $this->checkSiteUrl($feed); + + $this->findFeedTitle($xml, $feed); + $this->findFeedDescription($xml, $feed); + $this->findFeedLanguage($xml, $feed); + $this->findFeedId($xml, $feed); + $this->findFeedDate($xml, $feed); + $this->findFeedLogo($xml, $feed); + $this->findFeedIcon($xml, $feed); + + foreach ($this->getItemsTree($xml) as $entry) { + $entry = $this->registerSupportedNamespaces($entry); + + $item = new Item(); + $item->xml = $entry; + $item->namespaces = $this->used_namespaces; + + $this->findItemAuthor($xml, $entry, $item); + $this->findItemAuthorUrl($xml, $entry, $item); + + $this->findItemUrl($entry, $item); + $this->checkItemUrl($feed, $item); + + $this->findItemTitle($entry, $item); + $this->findItemContent($entry, $item); + + // Id generation can use the item url/title/content (order is important) + $this->findItemId($entry, $item, $feed); + $this->findItemDate($entry, $item, $feed); + $this->findItemEnclosure($entry, $item, $feed); + $this->findItemLanguage($entry, $item, $feed); + $this->findItemCategories($entry, $item, $feed); + + $this->itemPostProcessor->execute($feed, $item); + $feed->items[] = $item; + } + + Logger::setMessage(get_called_class().PHP_EOL.$feed); + + return $feed; + } + + /** + * Check if the feed url is correct. + * + * @param Feed $feed Feed object + */ + public function checkFeedUrl(Feed $feed) + { + if ($feed->getFeedUrl() === '') { + $feed->feedUrl = $this->fallback_url; + } else { + $feed->feedUrl = Url::resolve($feed->getFeedUrl(), $this->fallback_url); + } + } + + /** + * Check if the site url is correct. + * + * @param Feed $feed Feed object + */ + public function checkSiteUrl(Feed $feed) + { + if ($feed->getSiteUrl() === '') { + $feed->siteUrl = Url::base($feed->getFeedUrl()); + } else { + $feed->siteUrl = Url::resolve($feed->getSiteUrl(), $this->fallback_url); + } + } + + /** + * Check if the item url is correct. + * + * @param Feed $feed Feed object + * @param Item $item Item object + */ + public function checkItemUrl(Feed $feed, Item $item) + { + $item->url = Url::resolve($item->getUrl(), $feed->getSiteUrl()); + } + + /** + * Find the item date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $this->findItemPublishedDate($entry, $item, $feed); + $this->findItemUpdatedDate($entry, $item, $feed); + + if ($item->getPublishedDate() === null) { + // Use the updated date if available, otherwise use the feed date + $item->setPublishedDate($item->getUpdatedDate() ?: $feed->getDate()); + } + + if ($item->getUpdatedDate() === null) { + // Use the published date as fallback + $item->setUpdatedDate($item->getPublishedDate()); + } + + // Use the most recent of published and updated dates + $item->setDate(max($item->getPublishedDate(), $item->getUpdatedDate())); + } + + /** + * Get Item Post Processor instance + * + * @access public + * @return ItemPostProcessor + */ + public function getItemPostProcessor() + { + return $this->itemPostProcessor; + } + + /** + * Get DateParser instance + * + * @access public + * @return DateParser + */ + public function getDateParser() + { + if ($this->dateParser === null) { + $this->dateParser = new DateParser($this->config); + } + + return $this->dateParser; + } + + /** + * Generate a unique id for an entry (hash all arguments). + * + * @return string + */ + public function generateId() + { + return hash($this->hash_algo, implode(func_get_args())); + } + + /** + * Return true if the given language is "Right to Left". + * + * @static + * @param string $language Language: fr-FR, en-US + * @return bool + */ + public static function isLanguageRTL($language) + { + $language = strtolower($language); + + $rtl_languages = array( + 'ar', // Arabic (ar-**) + 'fa', // Farsi (fa-**) + 'ur', // Urdu (ur-**) + 'ps', // Pashtu (ps-**) + 'syr', // Syriac (syr-**) + 'dv', // Divehi (dv-**) + 'he', // Hebrew (he-**) + 'yi', // Yiddish (yi-**) + ); + + foreach ($rtl_languages as $prefix) { + if (strpos($language, $prefix) === 0) { + return true; + } + } + + return false; + } + + /** + * Set Hash algorithm used for id generation. + * + * @param string $algo Algorithm name + * @return \PicoFeed\Parser\Parser + */ + public function setHashAlgo($algo) + { + $this->hash_algo = $algo ?: $this->hash_algo; + return $this; + } + + /** + * Set config object. + * + * @param \PicoFeed\Config\Config $config Config instance + * @return \PicoFeed\Parser\Parser + */ + public function setConfig($config) + { + $this->config = $config; + $this->itemPostProcessor->setConfig($config); + return $this; + } + + /** + * Enable the content grabber. + * + * @return \PicoFeed\Parser\Parser + */ + public function disableContentFiltering() + { + $this->itemPostProcessor->unregister('PicoFeed\Processor\ContentFilterProcessor'); + return $this; + } + + /** + * Enable the content grabber. + * + * @param bool $needsRuleFile true if only pages with rule files should be + * scraped + * @param null|\Closure $scraperCallback Callback function that gets called for each + * scraper execution + * @return \PicoFeed\Parser\Parser + */ + public function enableContentGrabber($needsRuleFile = false, $scraperCallback = null) + { + $processor = new ScraperProcessor($this->config); + + if ($needsRuleFile) { + $processor->getScraper()->disableCandidateParser(); + } + + if ($scraperCallback !== null) { + $processor->setExecutionCallback($scraperCallback); + } + + $this->itemPostProcessor->register($processor); + return $this; + } + + /** + * Set ignored URLs for the content grabber. + * + * @param array $urls URLs + * @return \PicoFeed\Parser\Parser + */ + public function setGrabberIgnoreUrls(array $urls) + { + $this->itemPostProcessor->getProcessor('PicoFeed\Processor\ScraperProcessor')->ignoreUrls($urls); + return $this; + } + + /** + * Register all supported namespaces to be used within an xpath query. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement + */ + public function registerSupportedNamespaces(SimpleXMLElement $xml) + { + foreach ($this->namespaces as $prefix => $ns) { + $xml->registerXPathNamespace($prefix, $ns); + } + + return $xml; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php new file mode 100644 index 0000000..b5fbb69 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php @@ -0,0 +1,15 @@ + 'http://purl.org/rss/1.0/', + 'dc' => 'http://purl.org/dc/elements/1.1/', + 'content' => 'http://purl.org/rss/1.0/modules/content/', + 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement[] + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'rss:item', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'item') + ?: $xml->item; + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl(''); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'rss:channel/rss:link', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/link') + ?: $xml->channel->link; + + $feed->setSiteUrl(XmlParser::getValue($value)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $description = XmlParser::getXPathResult($xml, 'rss:channel/rss:description', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/description') + ?: $xml->channel->description; + + $feed->setDescription(XmlParser::getValue($description)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $logo = XmlParser::getXPathResult($xml, 'rss:image/rss:url', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'image/url'); + + $feed->setLogo(XmlParser::getValue($logo)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $feed->setIcon(''); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'rss:channel/rss:title', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/title') + ?: $xml->channel->title; + + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $language = XmlParser::getXPathResult($xml, 'rss:channel/dc:language', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:language', $this->namespaces); + + $feed->setLanguage(XmlParser::getValue($language)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl()); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $date = XmlParser::getXPathResult($xml, 'rss:channel/dc:date', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:date', $this->namespaces); + + $feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($date))); + } + + /** + * Find the item published date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'dc:date', $this->namespaces); + + $item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($date)) : null); + } + + /** + * Find the item updated date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if ($item->publishedDate === null) { + $this->findItemPublishedDate($entry, $item, $feed); + } + $item->setUpdatedDate($item->getPublishedDate()); // No updated date in RSS 1.0 specifications + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $title = XmlParser::getXPathResult($entry, 'rss:title', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'title') + ?: $entry->title; + + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $author = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'rss:channel/dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces); + + $item->setAuthor(XmlParser::getValue($author)); + } + + /** + * Find the item author URL. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + // There appears to be no support for author URL in the dc: terms + $item->setAuthorUrl(''); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces); + + if (XmlParser::getValue($content) === '') { + $content = XmlParser::getXPathResult($entry, 'rss:description', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'description') + ?: $entry->description; + } + + $item->setContent(XmlParser::getValue($content)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'rss:link', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'link') + ?: $entry->link; + + $item->setUrl(XmlParser::getValue($link)); + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); + + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Find the item categories. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param Feed $feed Feed object + */ + public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $categories = XmlParser::getXPathResult($entry, 'dc:subject', $this->namespaces); + $item->setCategoriesFromXml($categories); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php new file mode 100644 index 0000000..da9c0d5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php @@ -0,0 +1,330 @@ + 'http://purl.org/dc/elements/1.1/', + 'content' => 'http://purl.org/rss/1.0/modules/content/', + 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0', + 'atom' => 'http://www.w3.org/2005/Atom', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement[] + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'channel/item'); + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl(''); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/link'); + $feed->setSiteUrl(XmlParser::getValue($value)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/description'); + $feed->setDescription(XmlParser::getValue($value)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/image/url'); + $feed->setLogo(XmlParser::getValue($value)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $feed->setIcon(''); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'channel/title'); + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/language'); + $feed->setLanguage(XmlParser::getValue($value)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl()); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $publish_date = XmlParser::getXPathResult($xml, 'channel/pubDate'); + $update_date = XmlParser::getXPathResult($xml, 'channel/lastBuildDate'); + + $published = !empty($publish_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($publish_date)) : null; + $updated = !empty($update_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($update_date)) : null; + + if ($published === null && $updated === null) { + $feed->setDate($this->getDateParser()->getCurrentDateTime()); // We use the current date if there is no date for the feed + } elseif ($published !== null && $updated !== null) { + $feed->setDate(max($published, $updated)); // We use the most recent date between published and updated + } else { + $feed->setDate($updated ?: $published); + } + } + + /** + * Find the item published date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'pubDate'); + + $item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($date)) : null); + } + + /** + * Find the item updated date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if ($item->publishedDate === null) { + $this->findItemPublishedDate($entry, $item, $feed); + } + $item->setUpdatedDate($item->getPublishedDate()); // No updated date in RSS 2.0 specifications + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $value = XmlParser::getXPathResult($entry, 'title'); + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($value)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $value = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author') + ?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/managingEditor'); + + $item->setAuthor(XmlParser::getValue($value)); + } + + /** + * Find the item author URL. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + // There appears to be no support for author URL in the dc: terms or author element + $item->setAuthorUrl(''); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces); + + if (XmlParser::getValue($content) === '') { + $content = XmlParser::getXPathResult($entry, 'description'); + } + + $item->setContent(XmlParser::getValue($content)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'link') + ?: XmlParser::getXPathResult($entry, 'atom:link/@href', $this->namespaces); + + if (!empty($link)) { + $item->setUrl(XmlParser::getValue($link)); + } else { + $link = XmlParser::getXPathResult($entry, 'guid'); + $link = XmlParser::getValue($link); + + if (filter_var($link, FILTER_VALIDATE_URL) !== false) { + $item->setUrl($link); + } + } + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $id = XmlParser::getValue(XmlParser::getXPathResult($entry, 'guid')); + + if ($id) { + $item->setId($this->generateId($id)); + } else { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if (isset($entry->enclosure)) { + $type = XmlParser::getXPathResult($entry, 'enclosure/@type'); + $url = XmlParser::getXPathResult($entry, 'feedburner:origEnclosureLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'enclosure/@url'); + + $item->setEnclosureUrl(Url::resolve(XmlParser::getValue($url), $feed->getSiteUrl())); + $item->setEnclosureType(XmlParser::getValue($type)); + } + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Find the item categories. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param Feed $feed Feed object + */ + public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $categories = XmlParser::getXPathResult($entry, 'category'); + $item->setCategoriesFromXml($categories); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php new file mode 100644 index 0000000..058fca1 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php @@ -0,0 +1,13 @@ +childNodes->length === 0) { + return false; + } + + return $dom; + } + + /** + * Small wrapper around Laminas Xml to turn their exceptions into PicoFeed exceptions + * + * @static + * @access private + * @param string $input + * @param DOMDocument $dom + * @throws XmlEntityException + * @return SimpleXMLElement|DomDocument|boolean + */ + private static function scan($input, $dom = null) + { + try { + return Security::scan($input, $dom); + } catch(RuntimeException $e) { + throw new XmlEntityException($e->getMessage()); + } + } + + /** + * Load HTML document by using a DomDocument instance or return false on failure. + * + * @static + * @access public + * @param string $input XML content + * @return DOMDocument + */ + public static function getHtmlDocument($input) + { + $dom = new DomDocument(); + + if (empty($input)) { + return $dom; + } + + libxml_use_internal_errors(true); + + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $dom->loadHTML($input, LIBXML_NONET); + } else { + $dom->loadHTML($input); + } + + self::$errors = []; + foreach (libxml_get_errors() as $error) { + self::$errors[] = sprintf('XML error: %s (Line: %d - Column: %d - Code: %d)', + $error->message, + $error->line, + $error->column, + $error->code + ); + } + + libxml_use_internal_errors(false); + + return $dom; + } + + /** + * Convert a HTML document to XML. + * + * @static + * @access public + * @param string $html HTML document + * @return string + */ + public static function htmlToXml($html) + { + $dom = self::getHtmlDocument(''.$html); + return $dom->saveXML($dom->getElementsByTagName('body')->item(0)); + } + + /** + * Get XML parser errors. + * + * @static + * @access public + * @return string + */ + public static function getErrors() + { + return implode(', ', self::$errors); + } + + /** + * Get the encoding from a xml tag. + * + * @static + * @access public + * @param string $data Input data + * @return string + */ + public static function getEncodingFromXmlTag($data) + { + $encoding = ''; + + if (strpos($data, '')); + $data = str_replace("'", '"', $data); + + $p1 = strpos($data, 'encoding='); + $p2 = strpos($data, '"', $p1 + 10); + + if ($p1 !== false && $p2 !== false) { + $encoding = substr($data, $p1 + 10, $p2 - $p1 - 10); + $encoding = strtolower($encoding); + } + } + + return $encoding; + } + + /** + * Get the charset from a meta tag. + * + * @static + * @access public + * @param string $data Input data + * @return string + */ + public static function getEncodingFromMetaTag($data) + { + $encoding = ''; + + if (preg_match('/;]+)/i', $data, $match) === 1) { + $encoding = strtolower($match[1]); + } + + return $encoding; + } + + /** + * Rewrite XPath query to use namespace-uri and local-name derived from prefix. + * + * @static + * @access public + * @param string $query XPath query + * @param array $ns Prefix to namespace URI mapping + * @return string + */ + public static function replaceXPathPrefixWithNamespaceURI($query, array $ns) + { + return preg_replace_callback('/([A-Z0-9]+):([A-Z0-9]+)/iu', function ($matches) use ($ns) { + // don't try to map the special prefix XML + if (strtolower($matches[1]) === 'xml') { + return $matches[0]; + } + + return '*[namespace-uri()="'.$ns[$matches[1]].'" and local-name()="'.$matches[2].'"]'; + }, + $query); + } + + /** + * Get the result elements of a XPath query. + * + * @static + * @access public + * @param SimpleXMLElement $xml XML element + * @param string $query XPath query + * @param array $ns Prefix to namespace URI mapping + * @return SimpleXMLElement[] + */ + public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array()) + { + if (!empty($ns)) { + $query = static::replaceXPathPrefixWithNamespaceURI($query, $ns); + } + + return $xml->xpath($query); + } + + /** + * Get the first Xpath result or SimpleXMLElement value + * + * @static + * @access public + * @param mixed $value + * @return string + */ + public static function getValue($value) + { + $result = ''; + + if (is_array($value) && count($value) > 0) { + $result = (string) $value[0]; + } elseif (is_a($value, 'SimpleXMLElement')) { + return $result = (string) $value; + } + + return trim($result); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php new file mode 100644 index 0000000..2de9e4b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php @@ -0,0 +1,14 @@ +config->getContentFiltering(true)) { + $filter = Filter::html($item->getContent(), $feed->getSiteUrl()); + $filter->setConfig($this->config); + $item->setContent($filter->execute()); + } else { + Logger::setMessage(get_called_class().': Content filtering disabled'); + } + + return false; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php new file mode 100644 index 0000000..49adf9c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php @@ -0,0 +1,49 @@ +generators as $generator) { + $className = '\PicoFeed\Generator\\'.ucfirst($generator).'ContentGenerator'; + $object = new $className($this->config); + + if ($object->execute($item)) { + return true; + } + } + + return false; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php new file mode 100644 index 0000000..7b092b5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php @@ -0,0 +1,106 @@ +processors as $processor) { + if ($processor->execute($feed, $item)) { + return true; + } + } + + return false; + } + + /** + * Register a new Item post-processor + * + * @access public + * @param ItemProcessorInterface $processor + * @return ItemPostProcessor + */ + public function register(ItemProcessorInterface $processor) + { + $this->processors[get_class($processor)] = $processor; + return $this; + } + + /** + * Remove Processor instance + * + * @access public + * @param string $class + * @return ItemPostProcessor + */ + public function unregister($class) + { + if (isset($this->processors[$class])) { + unset($this->processors[$class]); + } + + return $this; + } + + /** + * Checks whether a specific processor is registered or not + * + * @access public + * @param string $class + * @return bool + */ + public function hasProcessor($class) + { + return isset($this->processors[$class]); + } + + /** + * Get Processor instance + * + * @access public + * @param string $class + * @return ItemProcessorInterface|null + */ + public function getProcessor($class) + { + return isset($this->processors[$class]) ? $this->processors[$class] : null; + } + + public function setConfig(Config $config) + { + foreach ($this->processors as $processor) { + $processor->setConfig($config); + } + + return false; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php new file mode 100644 index 0000000..5d53226 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php @@ -0,0 +1,25 @@ +executionCallback = $executionCallback; + return $this; + } + + /** + * Execute Item Processor + * + * @access public + * @param Feed $feed + * @param Item $item + * @return bool + */ + public function execute(Feed $feed, Item $item) + { + if (!in_array($item->getUrl(), $this->ignoredUrls)) { + $scraper = $this->getScraper(); + $scraper->setUrl($item->getUrl()); + $scraper->execute(); + + if ($this->executionCallback && is_callable($this->executionCallback)) { + call_user_func($this->executionCallback, $feed, $item, $scraper); + } + + if ($scraper->hasRelevantContent()) { + $item->setContent($scraper->getFilteredContent()); + } + } + + return false; + } + + /** + * Ignore list of URLs + * + * @access public + * @param array $urls + * @return $this + */ + public function ignoreUrls(array $urls) + { + $this->ignoredUrls = $urls; + return $this; + } + + /** + * Returns Scraper instance + * + * @access public + * @return Scraper + */ + public function getScraper() + { + if ($this->scraper === null) { + $this->scraper = new Scraper($this->config); + } + + return $this->scraper; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php new file mode 100644 index 0000000..d4ca07d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php @@ -0,0 +1,186 @@ +content; + } + + /** + * Get the icon file type (available only after the download). + * + * @return string + */ + public function getType() + { + foreach ($this->types as $type) { + if (strpos($this->content_type, $type) === 0) { + return $type; + } + } + + return 'image/x-icon'; + } + + /** + * Get data URI (http://en.wikipedia.org/wiki/Data_URI_scheme). + * + * @return string + */ + public function getDataUri() + { + if (empty($this->content)) { + return ''; + } + + return sprintf( + 'data:%s;base64,%s', + $this->getType(), + base64_encode($this->content) + ); + } + + /** + * Download and check if a resource exists. + * + * @param string $url URL + * @return \PicoFeed\Client\Client Client instance + */ + public function download($url) + { + $client = Client::getInstance(); + $client->setConfig($this->config); + + Logger::setMessage(get_called_class().' Download => '.$url); + + try { + $client->execute($url); + } catch (ClientException $e) { + Logger::setMessage(get_called_class().' Download Failed => '.$e->getMessage()); + } + + return $client; + } + + /** + * Check if a remote file exists. + * + * @param string $url URL + * @return bool + */ + public function exists($url) + { + return $this->download($url)->getContent() !== ''; + } + + /** + * Get the icon link for a website. + * + * @param string $website_link URL + * @param string $favicon_link optional URL + * @return string + */ + public function find($website_link, $favicon_link = '') + { + $website = new Url($website_link); + + if ($favicon_link !== '') { + $icons = array($favicon_link); + } else { + $icons = $this->extract($this->download($website->getBaseUrl('/'))->getContent()); + $icons[] = $website->getBaseUrl('/favicon.ico'); + } + + foreach ($icons as $icon_link) { + $icon_link = Url::resolve($icon_link, $website); + $resource = $this->download($icon_link); + $this->content = $resource->getContent(); + $this->content_type = $resource->getContentType(); + + if ($this->content !== '') { + return $icon_link; + } elseif ($favicon_link !== '') { + return $this->find($website_link); + } + } + + return ''; + } + + /** + * Extract the icon links from the HTML. + * + * @param string $html HTML + * @return array + */ + public function extract($html) + { + $icons = array(); + + if (empty($html)) { + return $icons; + } + + $dom = XmlParser::getHtmlDocument($html); + + $xpath = new DOMXpath($dom); + $elements = $xpath->query('//link[@rel="icon" or @rel="shortcut icon" or @rel="Shortcut Icon" or @rel="icon shortcut"]'); + + for ($i = 0; $i < $elements->length; ++$i) { + $icons[] = $elements->item($i)->getAttribute('href'); + } + + return $icons; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php new file mode 100644 index 0000000..596b02d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php @@ -0,0 +1,189 @@ + '//feed', + 'Rss20' => '//rss[@version="2.0"]', + 'Rss92' => '//rss[@version="0.92"]', + 'Rss91' => '//rss[@version="0.91"]', + 'Rss10' => '//rdf', + ); + + /** + * Download a feed (no discovery). + * + * @param string $url Feed url + * @param string $last_modified Last modified HTTP header + * @param string $etag Etag HTTP header + * @param string $username HTTP basic auth username + * @param string $password HTTP basic auth password + * + * @return \PicoFeed\Client\Client + */ + public function download($url, $last_modified = '', $etag = '', $username = '', $password = '') + { + $url = $this->prependScheme($url); + + return Client::getInstance() + ->setConfig($this->config) + ->setLastModified($last_modified) + ->setEtag($etag) + ->setUsername($username) + ->setPassword($password) + ->execute($url); + } + + /** + * Discover and download a feed. + * + * @param string $url Feed or website url + * @param string $last_modified Last modified HTTP header + * @param string $etag Etag HTTP header + * @param string $username HTTP basic auth username + * @param string $password HTTP basic auth password + * @return Client + * @throws SubscriptionNotFoundException + */ + public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '') + { + $client = $this->download($url, $last_modified, $etag, $username, $password); + + // It's already a feed or the feed was not modified + if (!$client->isModified() || $this->detectFormat($client->getContent())) { + return $client; + } + + // Try to find a subscription + $links = $this->find($client->getUrl(), $client->getContent()); + + if (empty($links)) { + throw new SubscriptionNotFoundException('Unable to find a subscription'); + } + + return $this->download($links[0], $last_modified, $etag, $username, $password); + } + + /** + * Find feed urls inside a HTML document. + * + * @param string $url Website url + * @param string $html HTML content + * + * @return array List of feed links + */ + public function find($url, $html) + { + Logger::setMessage(get_called_class().': Try to discover subscriptions'); + + $dom = XmlParser::getHtmlDocument($html); + $xpath = new DOMXPath($dom); + $links = array(); + + $queries = array( + '//link[@type="application/rss+xml"]', + '//link[@type="application/atom+xml"]', + ); + + foreach ($queries as $query) { + $nodes = $xpath->query($query); + + foreach ($nodes as $node) { + $link = $node->getAttribute('href'); + + if (!empty($link)) { + $feedUrl = new Url($link); + $siteUrl = new Url($url); + + $links[] = $feedUrl->getAbsoluteUrl($feedUrl->isRelativeUrl() ? $siteUrl->getBaseUrl() : ''); + } + } + } + + Logger::setMessage(get_called_class().': '.implode(', ', $links)); + + return $links; + } + + /** + * Get a parser instance. + * + * @param string $url Site url + * @param string $content Feed content + * @param string $encoding HTTP encoding + * @return \PicoFeed\Parser\Parser + * @throws UnsupportedFeedFormatException + */ + public function getParser($url, $content, $encoding) + { + $format = $this->detectFormat($content); + + if (empty($format)) { + throw new UnsupportedFeedFormatException('Unable to detect feed format'); + } + + $className = '\PicoFeed\Parser\\'.$format; + + $parser = new $className($content, $encoding, $url); + $parser->setHashAlgo($this->config->getParserHashAlgo()); + $parser->setConfig($this->config); + + return $parser; + } + + /** + * Detect the feed format. + * + * @param string $content Feed content + * @return string + */ + public function detectFormat($content) + { + $dom = XmlParser::getHtmlDocument($content); + $xpath = new DOMXPath($dom); + + foreach ($this->formats as $parser_name => $query) { + $nodes = $xpath->query($query); + + if ($nodes->length === 1) { + return $parser_name; + } + } + + return ''; + } + + /** + * Add the prefix "http://" if the end-user just enter a domain name. + * + * @param string $url Url + * @return string + */ + public function prependScheme($url) + { + if (!preg_match('%^https?://%', $url)) { + $url = 'http://'.$url; + } + + return $url; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php new file mode 100644 index 0000000..4f03dbe --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://combat.blog.lemonde.fr/2013/08/31/teddy-riner-le-rookie-devenu-rambo/#xtor=RSS-3208', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "fb-like") or contains(@class, "social")]' + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php new file mode 100644 index 0000000..ee641b0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'title' => '//header/h1', + 'test_url' => 'http://bits.blogs.nytimes.com/2012/01/16/wikipedia-plans-to-go-dark-on-wednesday-to-protest-sopa/', + 'body' => array( + '//div[@class="postContent"]', + ), + 'strip' => array( + '//*[@class="shareToolsBox"]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php new file mode 100644 index 0000000..f2028f4 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.igen.fr/ailleurs/2014/05/nvidia-va-delaisser-les-smartphones-grand-public-86031', + 'body' => array( + '//div[contains(@class, "field-name-body")]' + ), + 'strip' => array( + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php new file mode 100644 index 0000000..8ff921c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nytimes.com/2011/05/15/world/middleeast/15prince.html', + 'body' => array( + '//p[contains(@class, "story-content")] | //div[@class="image"]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php new file mode 100644 index 0000000..cc5d83c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://eliascarpe.over-blog.com/2015/12/re-upload-projets-d-avenir.html', + 'body' => array( + '//div[contains(concat(" ", normalize-space(@class), " "), " ob-section ")]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php new file mode 100644 index 0000000..66713f7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.phoronix.com/scan.php?page=article&item=amazon_ec2_bare&num=1', + 'body' => array( + '//div[@class="content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php new file mode 100644 index 0000000..a795bca --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.slate.com/articles/business/moneybox/2013/08/microsoft_ceo_steve_ballmer_retires_a_firsthand_account_of_the_company_s.html', + 'body' => array( + '//div[@class="sl-art-body"]', + ), + 'strip' => array( + '//*[contains(@class, "social") or contains(@class, "comments") or contains(@class, "sl-article-floatin-tools") or contains(@class, "sl-art-pag")]', + '//*[@id="mys_slate_logged_in"]', + '//*[@id="sl_article_tools_myslate_bottom"]', + '//*[@id="mys_myslate"]', + '//*[@class="sl-viral-container"]', + '//*[@class="sl-art-creds-cntr"]', + '//*[@class="sl-art-ad-midflex"]', + ) + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php new file mode 100644 index 0000000..e0d6f3f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theguardian.com/sustainable-business/2015/feb/02/2015-hyper-transparency-global-business', + 'body' => array( + '//div[contains(@class, "content__main-column--article")]', + ), + 'strip' => array( + '//div[contains(@class, "meta-container")]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php new file mode 100644 index 0000000..7b8f76e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php @@ -0,0 +1,29 @@ + array( + '%.*%' => array( + 'test_url' => 'https://en.wikipedia.org/wiki/Grace_Hopper', + 'body' => array( + '//div[@id="bodyContent"]', + ), + 'strip' => array( + "//div[@id='toc']", + "//div[@id='catlinks']", + "//div[@id='jump-to-nav']", + "//div[@class='thumbcaption']//div[@class='magnify']", + "//table[@class='navbox']", + "//table[contains(@class, 'infobox')]", + "//div[@class='dablink']", + "//div[@id='contentSub']", + "//div[@id='siteSub']", + "//table[@id='persondata']", + "//table[contains(@class, 'metadata')]", + "//*[contains(@class, 'noprint')]", + "//*[contains(@class, 'printfooter')]", + "//*[contains(@class, 'editsection')]", + "//*[contains(@class, 'error')]", + "//span[@title='pronunciation:']", + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php new file mode 100644 index 0000000..952b09a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php @@ -0,0 +1,44 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.wired.com/gamelife/2013/09/ouya-free-the-games/', + 'body' => array( + '//div[@data-js="gallerySlides"]', + '//div[starts-with(@class,"post")]', + ), + 'strip' => array( + '//h1', + '//nav', + '//button', + '//figure[starts-with(@class,"rad-slide")]', + '//figure[starts-with(@class,"end-slate")]', + '//div[contains(@class,"mobile-")]', + '//div[starts-with(@class,"mob-gallery-launcher")]', + '//div[contains(@id,"mobile-")]', + '//span[contains(@class,"slide-count")]', + '//div[contains(@class,"show-ipad")]', + '//img[contains(@id,"-hero-bg")]', + '//div[@data-js="overlayWrap"]', + '//ul[contains(@class,"metadata")]', + '//div[@class="opening center"]', + '//p[contains(@class="byline-mob"]', + '//div[@id="o-gallery"]', + '//div[starts-with(@class,"sm-col")]', + '//div[contains(@class,"pad-b-huge")]', + '//a[contains(@class,"visually-hidden")]', + '//*[@class="social"]', + '//i', + '//div[@data-js="mobGalleryAd"]', + '//div[contains(@class,"footer")]', + '//div[contains(@data-js,"fader")]', + '//div[@id="sharing"]', + '//div[contains(@id,"related")]', + '//div[@id="most-pop"]', + '//ul[@id="article-tags"]', + '//style', + '//section[contains(@class,"footer")]' + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php new file mode 100644 index 0000000..f6e6cc1 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://online.wsj.com/article/SB10001424127887324108204579023143974408428.html', + 'body' => array( + '//div[@class="articlePage"]', + ), + 'strip' => array( + '//*[@id="articleThumbnail_2"]', + '//*[@class="socialByline"]', + ) + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php new file mode 100644 index 0000000..6d144f0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.01net.com/editorial/624550/twitter-rachete-madbits-un-specialiste-francais-de-lanalyse-dimages/', + 'body' => array( + '//div[@class="article_ventre_box"]', + ), + 'strip' => array( + '//link', + '//*[contains(@class, "article_navigation")]', + '//h1', + '//*[contains(@class, "article_toolbarMain")]', + '//*[contains(@class, "article_imagehaute_box")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php new file mode 100644 index 0000000..6c269db --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://24.hu/belfold/2017/10/20/millios-lehuzasok-miatt-razziaztak-egy-budapesti-barban-videoval/', + 'body' => array( + '//div[@class="post-title-wrapper"]/h1', + '//div[@class="post-header-wrapper has-img"]/img', + '//div[@class="post-body"]' + + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php new file mode 100644 index 0000000..e65bad1 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'https://444.hu/2017/10/20/tulszamlazo-helyen-utottek-rajta-budapest-belvarosaban', + 'body' => array( + '//div[@id="headline"]/h1', + '//article' + ), + 'strip' => array( + '//article/footer', + '//article/div[@class="jeti-roadblock ad"]', + '//figcaption', + '//noscript', + '//ul[@class="widget-stream-items"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php new file mode 100644 index 0000000..68cfe05 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://888.hu/article-a-budapesti-szocik-nem-szeretik-a-videki-szocikat', + 'body' => array( + '//div[@id="cikkholder"]/h1', + '//div[@class="maincontent8"]' + ), + 'strip' => array( + '//div[@class="AdW"]', + '//h1[@class="olvastadmar"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php new file mode 100644 index 0000000..752d041 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%alt="(.+)" title="(.+)" */>%' => '/>
$1
$2', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php new file mode 100644 index 0000000..1e61fe6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.achgut.com/artikel/deutscher_herbst_wg_reichsstrasse_106', + 'body' => array( + '//div[@class="headerpict_half"]/div/img', + '//div[@class="beitrag"]/div[@class="teaser_blog_text"]' + ), + 'strip' => array( + '//div[@class="footer_blog_text"]', + '//div[@class="beitrag"]/div[@class="teaser_blog_text"]/h2[1]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php new file mode 100644 index 0000000..98d384e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php @@ -0,0 +1,23 @@ + array( + '%^/news.*%' => array( + 'test_url' => 'http://www.adventuregamers.com/news/view/31079', + 'body' => array( + '//div[@class="bodytext"]', + ) + ), + '%^/videos.*%' => array( + 'test_url' => 'http://www.adventuregamers.com/videos/view/31056', + 'body' => array( + '//iframe', + ) + ), + '%^/articles.*%' => array( + 'test_url' => 'http://www.adventuregamers.com/articles/view/31049', + 'body' => array( + '//div[@class="cleft"]', + ) + ) + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php new file mode 100644 index 0000000..f440b23 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.alainonline.net/news_details.php?lang=arabic&sid=18907', + 'body' => array( + '//div[@class="news_details"]', + ), + 'strip' => array( + '//div[@class="news_details"]/div/div[last()]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php new file mode 100644 index 0000000..c02eb21 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html', + 'body' => array( + '//article[@id="main-story"]', + ), + 'strip' => array( + '//script', + '//header', + '//ul', + '//section[contains(@class,"profile")]', + '//a[@target="_self"]', + '//div[contains(@id,"_2")]', + '//div[contains(@id,"_3")]', + '//img[@class="viewMode"]', + '//table[contains(@class,"in-article-item")]', + '//div[@data-embed-type="Brightcove"]', + '//div[@class="QuoteContainer"]', + '//div[@class="BottomByLine"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php new file mode 100644 index 0000000..e8a506d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html', + 'body' => array( + '//div[@class="story-body"]', + ), + 'strip' => array( + '//p[@class="kindofstory"]', + '//cite[@class="byline"]', + '//div[@class="useful-top"]', + '//div[contains(@class,"related-topics")]', + '//links', + '//sharebar', + '//related-topics', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php new file mode 100644 index 0000000..8ede99b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.allgemeine-zeitung.de/lokales/polizei/mainz-gonsenheim-unbekannte-rauben-esso-tankstelle-in-kurt-schumacher-strasse-aus_14913147.htm', + 'body' => array( + '//div[contains(@class, "article")][1]', + ), + 'strip' => array( + '//read/h1', + '//*[@id="t-map"]', + '//*[contains(@class, "modules")]', + '//*[contains(@class, "adsense")]', + '//*[contains(@class, "linkbox")]', + '//*[contains(@class, "info")]', + '//*[@class="skip"]', + '//*[@class="funcs"]', + '//span[@class="nd address"]', + '//a[contains(@href, "abo-und-services")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php new file mode 100644 index 0000000..51247f7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="comic_image"]', + '//div[@class="comment-wrapper"][position()=1]', + ), + 'strip' => array(), + 'test_url' => 'http://www.anythingcomic.com/comics/2108929/stress-free/', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php new file mode 100644 index 0000000..5bb2bb6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hosted.ap.org/dynamic/stories/A/AS_CHINA_GAO_ZHISHENG?SITE=AP&SECTION=HOME&TEMPLATE=DEFAULT', + 'body' => array( + '//img[@class="ap-smallphoto-img"]', + '//span[@class="entry-content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php new file mode 100644 index 0000000..fc56922 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.areadvd.de/news/daily-deals-angebote-bei-lautsprecher-teufel-3/', + 'body' => array('//div[contains(@class,"entry")]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php new file mode 100644 index 0000000..55e01ce --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://arstechnica.com/tech-policy/2015/09/judge-warners-2m-happy-birthday-copyright-is-bogus/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//h4[@class="post-upperdek"]', + '//h1', + '//ul[@class="lSPager lSGallery"]', + '//div[@class="lSAction"]', + '//section[@class="post-meta"]', + '//figcaption', + '//aside', + '//div[@class="gallery-image-credit"]', + '//section[@class="article-author"]', + '//*[contains(@id,"social-")]', + '//div[contains(@id,"footer")]', + ), + ), + ), +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php new file mode 100644 index 0000000..170ec4d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.atv.hu/belfold/20171020-tobb-millio-forintot-csalt-ki-egy-idos-ferfitol-a-budapesti-no', + 'body' => array( + '//article' + ), + 'strip' => array( + '//span[@class="date"]', + '//div[@class="fb-like db_iframe_widget"]', + '//div[@class="ad-wrapper dashed-border"]', + '//div[@class="footer-meta-wrapper"]', + '//div[@class="image-wrapper "]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php new file mode 100644 index 0000000..5ab7051 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php @@ -0,0 +1,10 @@ + array( + '%/index.php.*comic=.*%' => array( + 'test_url' => 'http://www.awkwardzombie.com/index.php?comic=041315', + 'body' => array('//*[@id="comic"]/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php new file mode 100644 index 0000000..bc5932a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'https://medium.com/lessons-learned/917b8b63ae3e', + 'body' => array( + '//div[contains(@class,"section-inner")]', + ), + 'strip' => array( + '//div[contains(@class,"metabar")]', + '//img[contains(@class,"thumbnail")]', + '//h1', + '//blockquote', + '//p[contains(@class,"graf-after--h4")]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php new file mode 100644 index 0000000..165515b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bangkokpost.com/news/politics/704204/new-us-ambassador-arrives-in-bangkok', + 'body' => array( + '//article/div[@class="articleContents"]', + ), + 'strip' => array( + '//h2', + '//h4', + '//div[@class="text-size"]', + '//div[@class="relate-story"]', + '//div[@class="text-ads"]', + '//ul', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php new file mode 100644 index 0000000..b191a6e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bauerwilli.com/intuitive-eating/', + 'body' => array( + '//div[@class="entry-thumbnail"]', + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//div[@class="tptn_counter"]', + '//div[contains(@class, "sharedaddy")]' + ), + ), + ), +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php new file mode 100644 index 0000000..7507a2f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://bgr.com/2015/09/27/iphone-6s-waterproof-testing/', + 'body' => array( + '//img[contains(@class,"img")]', + '//div[@class="text-column"]', + ), + 'strip' => array( + '//strong', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php new file mode 100644 index 0000000..55c4089 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php @@ -0,0 +1,31 @@ + array( + '%.*%' => array( + 'test_url' => 'http://bigpicture.ru/?p=556658', + 'body' => array( + '//div[@class="article container"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//h1', + '//*[@class="wp-smiley"]', + '//div[@class="ipmd"]', + '//div[@class="tags"]', + '//div[@class="social-button"]', + '//div[@class="bottom-share"]', + '//div[@class="raccoonbox"]', + '//div[@class="yndadvert"]', + '//div[@class="we-recommend"]', + '//div[@class="relap-bigpicture_ru-wrapper"]', + '//div[@id="mmail"]', + '//div[@id="mobile-ads-cut"]', + '//div[@id="liquidstorm-alt-html"]', + '//div[contains(@class, "post-tags")]', + '//*[contains(text(),"Смотрите также")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php new file mode 100644 index 0000000..d1cc3da --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bizjournals.com/milwaukee/news/2015/09/30/bucks-will-hike-prices-on-best-seats-at-new-arena.html', + 'body' => array( + '//figure/div/a/img', + '//p[@class="content__segment"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php new file mode 100644 index 0000000..d21aa98 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.biztimes.com/2017/02/10/settlement-would-revive-fowler-lake-condo-project-in-oconomowoc/', + 'body' => array( + '//h2/span[@class="subhead"]', + '//div[contains(@class,"article-content")]', + ), + 'strip' => array( + '//script', + '//div[contains(@class,"mobile-article-content")]', + '//div[contains(@class,"sharedaddy")]', + '//div[contains(@class,"author-details")]', + '//div[@class="row ad"]', + '//div[contains(@class,"relatedposts")]', + '//div[@class="col-lg-12"]', + '//div[contains(@class,"widget")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php new file mode 100644 index 0000000..7b74060 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.bleepingcomputer.com/news/google/chromes-sandbox-feature-infringes-on-three-patents-so-google-must-now-pay-20m/', + 'body' => array( + '//div[@class="article_section"]', + ), + 'strip' => array( + '//*[@itemprop="headline"]', + '//div[@class="cz-news-story-title-section"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php new file mode 100644 index 0000000..39c88ae --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.fefe.de/?ts=ad706a73', + 'body' => array( + '/html/body/ul', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php new file mode 100644 index 0000000..ce01651 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.mapillary.com/update/2015/08/26/traffic-sign-updates.html', + 'body' => array( + '//div[contains(@class, "blog-post__content")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php new file mode 100644 index 0000000..be406fa --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://m.brewers.mlb.com/news/article/161364798', + 'body' => array( + '//article[contains(@class,"article")]', + ), + 'strip' => array( + '//div[contains(@class,"ad-slot")]', + '//h1', + '//span[@class="timestamp"]', + '//div[contains(@class,"contributor-bottom")]', + '//div[contains(@class,"video")]', + '//ul[contains(@class,"social")]', + '//p[@class="tagline"]', + '//div[contains(@class,"social")]', + '//div[@class="button-wrap"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php new file mode 100644 index 0000000..4e73e79 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.buenosairesherald.com/article/199344/manzur-named-next-governor-of-tucum%C3%A1n', + 'body' => array( + '//div[@style="float:none"]', + ), + 'strip' => array( + '//div[contains(@class, "bz_alias_short_desc_container"]', + '//td[@id="bz_show_bug_column_1"]', + '//table[@id="attachment_table"]', + '//table[@class="bz_comment_table"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php new file mode 100644 index 0000000..ad83e43 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bunicomic.com/comic/buni-623/', + 'body' => array( + '//div[@class="comic-table"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php new file mode 100644 index 0000000..1f313cd --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://buttersafe.com/2015/04/21/the-incredible-flexible-man/', + 'body' => array( + '//div[@id="comic"]', + '//div[@class="post-comic"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php new file mode 100644 index 0000000..a631c97 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php @@ -0,0 +1,12 @@ + array( + '%/cad/.+%' => array( + 'test_url' => 'http://www.cad-comic.com/cad/20150417', + 'body' => array( + '//*[@id="content"]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php new file mode 100644 index 0000000..ea6191e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://chaoslife.findchaos.com/pets-in-the-wild', + 'body' => array('//div[@id="comic"]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php new file mode 100644 index 0000000..450117b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.chinafile.com/books/shanghai-faithful?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+chinafile%2FAll+%28ChinaFile%29', + 'body' => array( + '//div[contains(@class,"pane-featured-photo-panel-pane-1")]', + '//div[contains(@class,"video-above-fold")]', + '//div[@class="sc-media"]', + '//div[contains(@class,"field-name-body")]', + ), + 'strip' => array( + '//div[contains(@class,"cboxes")]', + '//div[contains(@class,"l-middle")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php new file mode 100644 index 0000000..2cd1b70 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://cicero.de/innenpolitik/plaene-der-eu-kommission-der-ganz-normale-terror', + 'body' => array( + '//p[@class="lead"]', + '//article/div[2]/div[contains(@class, "field--name-field-cc-image")]', + '//article/div[2]/div[contains(@class, "image-description")]', + '//div[@class="field field-name-field-cc-body"]', + ), + 'strip' => array( + '//*[contains(@class, "urban-ad-sign")]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php new file mode 100644 index 0000000..9dcc7e5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php @@ -0,0 +1,10 @@ + array( + '%/comic.*%' => array( + 'test_url' => 'http://cliquerefresh.com/comic/078-stating-the-obvious/', + 'body' => array('//div[@class="comicImg"]/img | //div[@class="comicImg"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php new file mode 100644 index 0000000..60767a5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php @@ -0,0 +1,37 @@ + array( + '%^/products.*%' => array( + 'test_url' => 'http://www.cnet.com/products/fibaro-flood-sensor/#ftag=CADf328eec', + 'body' => array( + '//li[contains(@class,"slide first"] || //figure[contains(@class,(promoFigure))]', + '//div[@class="quickInfo"]', + '//div[@class="col-6 ratings"]', + '//div[@id="editorReview"]', + ), + 'strip' => array( + '//script', + '//a[@class="clickToEnlarge"]', + '//div[@section="topSharebar"]', + '//div[contains(@class,"related")]', + '//div[contains(@class,"ad-")]', + '//div[@section="shortcodeGallery"]', + ), + ), + '%.*%' => array( + 'test_url' => 'http://cnet.com.feedsportal.com/c/34938/f/645093/s/4a340866/sc/28/l/0L0Scnet0N0Cnews0Cman0Eclaims0Eonline0Epsychic0Emade0Ehim0Ebuy0E10Emillion0Epowerball0Ewinning0Eticket0C0Tftag0FCAD590Aa51e/story01.htm', + 'body' => array( + '//p[@itemprop="description"]', + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//script', + '//a[@class="clickToEnlarge"]', + '//div[@section="topSharebar"]', + '//div[contains(@class,"related")]', + '//div[contains(@class,"ad-")]', + '//div[@section="shortcodeGallery"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php new file mode 100644 index 0000000..28a8bba --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://coinwelt.de/2017/08/bitcache-kreierer-kim-dotcom-bietet-arbeitsplaetze-fuer-blockchain-goetter/', + 'body' => array( + '//div[@class="post-inner"]//div[@class="entry"]', + ), + 'strip' => array( + '//div[contains(@class, "shariff")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php new file mode 100644 index 0000000..9209f9c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://consomac.fr/news-2430-l-iphone-6-toujours-un-secret-bien-garde.html', + 'body' => array( + '//div[contains(@id, "newscontent")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php new file mode 100644 index 0000000..88cef14 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.crash.net/motogp/news/885102/1/dovizioso-mugello-win-was-catalyst-for-title-challenge', + 'body' => array( + '//*[@id="block-system-main"]', + ), + 'strip' => array( + '//script', + '//style', + '//*[@class="social-bar"]', + '//*[@id="below-headline-image-ad"]', + '//*[@class="advert-"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php new file mode 100644 index 0000000..481e4b0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.csmonitor.com/USA/Politics/2015/0925/John-Boehner-steps-down-Self-sacrificing-but-will-it-lead-to-better-government', + 'body' => array( + '//h2[@id="summary"]', + '//div[@class="flex-video youtube"]', + '//div[contains(@class,"eza-body")]', + ), + 'strip' => array( + '//span[@id="breadcrumb"]', + '//div[@id="byline-wrapper"]', + '//div[@class="injection"]', + '//*[contains(@class,"promo_link")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php new file mode 100644 index 0000000..20eb1d7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dailyjs.com/2014/08/07/p5js/', + 'body' => array( + '//div[@id="post"]', + ), + 'strip' => array( + '//h2[@class="post"]', + '//div[@class="meta"]', + '//*[contains(@class, "addthis_toolbox")]', + '//*[contains(@class, "addthis_default_style")]', + '//*[@class="navigation small"]', + '//*[@id="related"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php new file mode 100644 index 0000000..db3fc0e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dailyreporter.com/2016/01/09/us-supreme-court-case-could-weaken-government-workers-unions/', + 'body' => array( + '//div[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//div[@class="dmcss_login_form"]', + '//*[contains(@class, "sharedaddy")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php new file mode 100644 index 0000000..5d1df4a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.dailytech.com/Apples+First+Fixes+to+iOS+9+Land+w+iOS++901+Release/article37495.htm', + 'body' => array( + '//div[@class="NewsBodyImage"]', + '//span[@id="lblSummary"]', + '//span[@id="lblBody"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php new file mode 100644 index 0000000..91f5c56 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.degroupnews.com/medias/vodsvod/amazon-concurrence-la-chromecast-de-google-avec-fire-tv-stick', + 'body' => array( + '//div[@class="contenu"]', + ), + 'strip' => array( + '//div[contains(@class, "a2a")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php new file mode 100644 index 0000000..7e95a51 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://derstandard.at/2000010267354/The-Witcher-3-Hohe-Hardware-Anforderungen-fuer-PC-Spieler?ref=rss', + 'body' => array( + '//div[@class="copytext"]', + '//ul[@id="media-list"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php new file mode 100644 index 0000000..b8e9b3d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@class="img-responsive img-comic"]', + ), + 'test_url' => 'http://dilbert.com/strip/2016-01-28', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php new file mode 100644 index 0000000..ae0dfe7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php @@ -0,0 +1,26 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blogs.discovermagazine.com/neuroskeptic/2017/01/25/publishers-jeffrey-beall/', + 'body' => array( + '//div[@class="contentWell"]', + ), + 'strip' => array( + '//h1', + '//div[@class="breadcrumbs"]', + '//div[@class="mobile"]', + '//div[@class="fromIssue"]', + '//div[contains(@class,"belowDeck")]', + '//div[@class="meta"]', + '//div[@class="shareIcons"]', + '//div[@class="categories"]', + '//div[@class="navigation"]', + '//div[@class="heading"]', + '//div[contains(@id,"-ad")]', + '//div[@class="relatedArticles"]', + '//div[@id="disqus_thread"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php new file mode 100644 index 0000000..aefc8f8 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://distrowatch.com/?newsid=08355', + 'body' => array( + '//td[@class="NewsText"][1]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php new file mode 100644 index 0000000..e116695 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dozodomo.com/bento/2014/03/04/lart-des-maki-de-takayo-kiyota/', + 'body' => array( + '//div[@class="joke"]', + '//div[@class="story-cover"]', + '//div[@class="story-content"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php new file mode 100644 index 0000000..cd30f2e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'body' => array('//img[@id="comicimage"]'), + 'strip' => array(), + 'test_url' => 'http://drawingboardcomic.com/index.php?comic=208', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php new file mode 100644 index 0000000..8139cc9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://e-w-e.ru/16-prekrasnyx-izobretenij-zhenshhin/', + 'body' => array( + '//div[contains(@class, "post_text")]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="views_post"]', + '//*[@class="adman_mobile"]', + '//*[@class="adman_desctop"]', + '//*[contains(@rel, "nofollow")]', + '//*[contains(@class, "wp-smiley")]', + '//*[contains(text(),"Источник:")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php new file mode 100644 index 0000000..522032f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.economist.com/blogs/buttonwood/2017/02/mixed-signals?fsrc=rss', + 'body' => array( + '//article', + ), + 'strip' => array( + '//span[@class="blog-post__siblings-list-header "]', + '//h1', + '//aside', + '//div[@class="blog-post__asideable-wrapper"]', + '//div[@class="share_inline_header"]', + '//div[@id="column-right"]', + '//div[contains(@class,"blog-post__siblings-list-aside")]', + '//div[@class="video-player__wrapper"]', + '//div[@class="blog-post__bottom-panel"]', + '//div[contains(@class,"latest-updates-panel__container")]', + '//div[contains(@class,"blog-post__asideable-content")]', + '//div[@aria-label="Advertisement"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php new file mode 100644 index 0000000..19bcbde --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://encyclopedie.naheulbeuk.com/article.php3?id_article=352', + 'body' => array( + '//td//h1[@class="titre-texte"]', + '//td//div[@class="surtitre"]', + '//td//div[@class="texte"]', + ), + ) + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php new file mode 100644 index 0000000..cf9e448 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.engadget.com/2015/04/20/dark-matter-discovery/?ncid=rss_truncated', + 'body' => array('//div[@id="page_body"]/div[@class="container@m-"]'), + 'strip' => array('//aside[@role="banner"]'), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php new file mode 100644 index 0000000..e86b59c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php @@ -0,0 +1,45 @@ + array( + '%/articles/view/comicsandcosplay/comics/critical-miss.*%' => array( + 'body' => array('//*[@class="body"]/span/img | //div[@class="folder_nav_links"]/following::p'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/critical-miss/13776-Critical-Miss-on-Framerates?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/namegame.*%' => array( + 'body' => array('//*[@class="body"]/span/p/img[@height != "120"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/namegame/9759-Leaving-the-Nest?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/stolen-pixels.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/stolen-pixels/8866-Stolen-Pixels-258-Where-the-Boys-Are?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/bumhugparade.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/bumhugparade/8262-Bumhug-Parade-13?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay.*/comics/escapistradiotheater%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/escapistradiotheater/8265-The-Escapist-Radio-Theater-13?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/paused.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img | //*[@class="body"]/span/div/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/paused/8263-Paused-16?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/fraughtwithperil.*%' => array( + 'body' => array('//*[@class="body"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/fraughtwithperil/12166-The-Escapist-Presents-Escapist-Comics-Critical-Miss-B-lyeh-Fhlop?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/video-games/columns/.*%' => array( + 'body' => array('//*[@id="article_content"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/video-games/columns/experienced-points/13971-What-50-Shades-and-Batman-Have-in-Common.2', + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php new file mode 100644 index 0000000..76a20f7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://espn.go.com/nfl/story/_/id/13388208/jason-whitlock-chip-kelly-controversy', + 'body' => array( + '//p', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php new file mode 100644 index 0000000..5adc59f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'body' => array('//a[@class="comic"]/img'), + 'strip' => array(), + 'test_url' => 'http://www.exocomics.com/379', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php new file mode 100644 index 0000000..3fdf02c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://explosm.net/comics/3803/', + 'body' => array( + '//div[@id="comic-container"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php new file mode 100644 index 0000000..12697cc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php new file mode 100644 index 0000000..a572061 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.factroom.ru/life/20-facts-about-oil', + 'body' => array( + '//div[@class="post"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//h1', + '//div[@id="yandex_ad2"]', + '//*[@class="jp-relatedposts"]', + '//div[contains(@class, "likely-desktop")]', + '//div[contains(@class, "likely-mobile")]', + '//p[last()]', + '//div[contains(@class, "facebook")]', + '//div[contains(@class, "desktop-underpost-direct")]', + '//div[contains(@class, "source-box")]', + '//div[contains(@class, "under-likely-desktop")]', + '//div[contains(@class, "mobile-down-post")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php new file mode 100644 index 0000000..74e70a8 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcodesign.com/3026548/exposure/peek-inside-the-worlds-forbidden-subway-tunnels', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php new file mode 100644 index 0000000..6916f28 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcoexist.com/3026114/take-a-seat-on-this-gates-funded-future-toilet-that-will-change-how-we-think-about-poop', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php new file mode 100644 index 0000000..e0869a2 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcompany.com/3026712/fast-feed/elon-musk-an-apple-tesla-merger-is-very-unlikely', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php new file mode 100644 index 0000000..20a47b2 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.ffworld.com/?rub=news&page=voir&id=2709', + 'body' => array( + '//div[@class="news_body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php new file mode 100644 index 0000000..3cbcddc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://foreignpolicy.com/2016/01/09/networking-giant-pulls-nsa-linked-code-exploited-by-hackers/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//div[@id="post-category"]', + '//div[@id="desktop-right"]', + '//h1', + '//section[@class="article-meta"]', + '//div[@class="side-panel-wrapper"]', + '//*[contains(@class, "share-")]', + '//*[contains(@id, "taboola-")]', + '//div[@class="comments"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php new file mode 100644 index 0000000..6ce4725 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://fossbytes.com/fbi-hacked-1000-computers-to-shut-down-largest-child-pornography-site-on-the-dark-web/', + 'body' => array( + '//div[@class="entry-inner"]', + ), + 'strip' => array( + '//*[@class="at-above-post addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="at-below-post addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="at-below-post-recommended addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="code-block code-block-12 ai-desktop"]', + '//*[@class="code-block code-block-13 ai-tablet-phone"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php new file mode 100644 index 0000000..ca2f85a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://fototelegraf.ru/?p=348232', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//div[@class="imageButtonsBlock"]', + '//div[@class="adOnPostBtwImg"]', + '//div[contains(@class, "post-tags")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php new file mode 100644 index 0000000..3f62f07 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'body' => array('//*[@id="comic"] | //*[@class="post-image"]'), + 'strip' => array(), + 'test_url' => 'http://www.fowllanguagecomics.com/comic/working-out/', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php new file mode 100644 index 0000000..8ab9c5c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.gamechannel.hu/cikk/hirblock/a-legacy-of-kain-feltamasztasara-keszul-a-crystal-dynamics', + 'body' => array( + '//div[@class="post"]/div[@class="entry"]' + ), + 'strip' => array( + '//div[@class="valaszto"]', + '//center/blockquote' // as we can't grab iframe here + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php new file mode 100644 index 0000000..56a4c72 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.gamestar.hu/hir/horizon-zero-dawn-the-frozen-wilds-vedjegy-239019.html', + 'body' => array( + '//article/header/h1', + '//div[@class="section section-2-3"]/div[@class="image"]/img', + '//article/p[@class="lead"]', + '//article/div[@class="content"]' + ), + 'strip' => array( + '//div[@class="ad ad-article-inside"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php new file mode 100644 index 0000000..d9ccecc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.geek.com/news/the-11-best-ways-to-eat-eggs-1634076/', + 'body' => array( + '//div[@class="articleinfo"]/figure', + '//div[@class="articleinfo"]/article', + '//span[@class="by"]', + ), + 'strip' => array( + '//span[@class="red"]', + '//div[@class="on-target"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php new file mode 100644 index 0000000..1954138 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://geektimes.ru/post/289151/', + 'body' => array( + "//div[contains(concat(' ',normalize-space(@class),' '),' content ')]" + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php new file mode 100644 index 0000000..44013b3 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@id="comic-1"]', + '//div[@class="entry"]', + ), + 'test_url' => 'http://gerbilwithajetpack.com/passing-the-digital-buck/', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php new file mode 100644 index 0000000..d9c3ae5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php @@ -0,0 +1,12 @@ + array( + '%/comics/oots.*%' => array( + 'test_url' => 'http://www.giantitp.com/comics/oots0989.html', + 'body' => array( + '//td[@align="center"]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php new file mode 100644 index 0000000..726634f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://github.com/audreyr/favicon-cheat-sheet', + 'body' => array( + '//article[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//h1', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php new file mode 100644 index 0000000..32960f0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.gocomics.com/pearlsbeforeswine/2015/05/30', + 'body' => array( + '//div[1]/p[1]/a[1]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php new file mode 100644 index 0000000..8731285 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.golem.de/news/breko-telekom-verzoegert-gezielt-den-vectoring-ausbau-1311-102974.html', + 'body' => array( + '//header[@class="cluster-header"]', + '//header[@class="paged-cluster-header"]', + '//div[@class="formatted"]', + ), + 'next_page' => array( + '//a[@id="atoc_next"]' + ), + 'strip' => array( + '//header[@class="cluster-header"]/a', + '//header[@class="cluster-header"]/h1', + '//div[@id="iqadtile4"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php new file mode 100644 index 0000000..62b1e3c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'https://gondola.hu/hirek/213754-A_budapesti_fejlesztesek_kerdese_mar_nem_zsakbamacska.html', + 'body' => array( + '//div[@id="cikk"]/div[@class="cim"]', + '//br[1]', + '//div[@class="alcim"]', + '//div[@class="lead"]', + '//div[@class="szoveg"]' + ), + 'strip' => array( + '//div[@class="ikonok"]', + '//div[@class="linkekblokk"]', + '//div[@id="billboardbanner"]', + '//div[@class="szerzo"]', + '//div[@class="kulcsszavak"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php new file mode 100644 index 0000000..4e43248 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://gorabbit.ru/article/10-oshchushcheniy-za-rulem-kogda-tolko-poluchil-voditelskie-prava', + 'body' => array( + '//div[@class="detail_text"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//div[@class="socials"]', + '//div[@id="cr_1"]', + '//div[@class="related_items"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php new file mode 100644 index 0000000..3f1ec16 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://habrahabr.ru/company/pentestit/blog/328606/', + 'body' => array( + "//div[contains(concat(' ',normalize-space(@class),' '),' content ')]" + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php new file mode 100644 index 0000000..75b0b83 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@id="comic"]', + '//div[@class="entry"]', + ), + 'strip' => array('//div[@class="ssba"]'), + 'test_url' => 'http://www.happletea.com/comic/mans-best-friend/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php new file mode 100644 index 0000000..56aec4f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php @@ -0,0 +1,11 @@ + array( + '%^/news.*%' => array( + 'test_url' => 'http://www.hardware.fr/news/14760/intel-lance-nouveaux-ssd-nand-3d.html', + 'body' => array( + '//div[@class="content_actualite"]/div[@class="md"]', + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php new file mode 100644 index 0000000..0ee6915 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php @@ -0,0 +1,79 @@ + array( + '%^/tp.*%' => array( + 'test_url' => 'https://www.heise.de/tp/features/Macrons-Vermoegenssteuer-Der-Staat-verzichtet-auf-3-2-Milliarden-3863931.html', + 'body' => array( + '//main/article' + ), + 'strip' => array( + '//header', + '//aside', + '//nav[@class="pre-akwa-toc"]', + '//*[@class="seite_zurueck"]', + '//*[@class="pagination"]', + '//a[@class="kommentare_lesen_link"]', + '//div[contains(@class, "shariff")]', + '//a[@class="beitragsfooter_permalink"]', + '//a[@class="beitragsfooter_fehlermelden"]', + '//a[@class="beitragsfooter_printversion"]' + ), + 'next_page' => array( + '//a[@class="seite_weiter"]' + ), + ), + '%^/newsticker/meldung.*%' => array( + 'test_url' => 'https://www.heise.de/newsticker/meldung/DragonFly-BSD-5-0-mit-experimentellem-HAMMER2-veroeffentlicht-3864731.html', + 'body' => array( + '//div[@class="article-content"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "video")]', + ), + ), + '%^/autos/artikel.*%' => array( + 'test_url' => 'https://www.heise.de/autos/artikel/Bericht-Mazda-baut-Range-Extender-mit-Wankelmotor-3864760.html', + 'body' => array( + '//section[@id="artikel_text"]' + ), + 'strip' => array( + '//p[@id="content_foren"]', + '//div[contains(@class, "shariff")]', + '//p[@class="permalink"]', + '//p[@class="printversion"]' + ), + ), + '%^/foto/meldung.*%' => array( + 'test_url' => 'https://www.heise.de/foto/meldung/Wildlife-Fotograf-des-Jahres-Gewinnerbild-zeigt-getoetetes-Nashorn-3864311.html', + 'body' => array( + '//div[@class="article-content"]' + ), + ), + '%^/ct.*%' => array( + 'test_url' => 'https://www.heise.de/ct/artikel/Google-Pixel-2-und-Pixel-2-XL-im-Test-3863842.html', + 'body' => array( + '//main/div[1]/div[1]/section' + ), + 'strip' => array( + '//header' + ) + ), + '%^/developer.*%' => array( + 'test_url' => 'https://www.heise.de/developer/meldung/Container-Docker-unterstuetzt-Kubernetes-3863625.html', + 'body' => array( + '//div[@class="article-content"]' + ) + ), + '%.*%' => array( + 'test_url' => 'https://www.heise.de/mac-and-i/meldung/iOS-App-Nude-findet-mittels-ML-Nacktbilder-und-versteckt-sie-3864217.html', + 'body' => array( + '//article/div[@class="meldung_wrapper"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "video")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php new file mode 100644 index 0000000..1a38809 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hirek.prim.hu/cikk/2017/10/02/atadtak_a_6_fenntarthatosagi_sajtodijat', + 'body' => array( + '//div[@class="boxbody article_box"]/h2', + '//div[@class="text_body"]' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php new file mode 100644 index 0000000..faf01f3 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'https://hotshowlife.com/top-10-chempionov-produktov-po-szhiganiyu-kalorij/', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//div[@class="ads2"]', + '//div[@class="mistape_caption"]', + '//div[contains(@class, "et_social_media_hidden")]', + '//div[contains(@class, "et_social_inline_bottom")]', + '//div[contains(@class, "avatar")]', + '//ul[contains(@class, "entry-tags")]', + '//div[contains(@class, "entry-meta")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php new file mode 100644 index 0000000..b52b07b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.huffingtonpost.com/2014/02/20/centscere-social-media-syracuse_n_4823848.html', + 'body' => array( + '//article[@class="content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php new file mode 100644 index 0000000..efc9371 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hvg.hu/brandchannel/mastercardbch_20171006_Egyetlen_mobillal_erintettuk_Budapest_legjobb_gasztrohelyeit', + 'body' => array( + '//div[@class="article-title article-title"]', + '//div[@class="article-cover-img"]', + '//div[@class="article-main"]' + ), + 'strip' => array( + '//figcaption', + '//div[@class="article-info byline"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php new file mode 100644 index 0000000..06cae09 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.idokep.hu/hirek/4-es-erossegu-tajfun-tart-japan-fele', + 'body' => array( + '//div[@class="cikk-title"]/h3', + '//div[@class="lead"]', + '//div[@class="atvett_tartalom"]', + '//div[@class="cikk-tartalom"]' + ), + 'strip' => array( + '//div[@class="cimkes-doboz"]', + '//div[@class="komment-wrap"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php new file mode 100644 index 0000000..727d7b7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php @@ -0,0 +1,29 @@ + array( + '%.*%' => array( + 'test_url' => 'http://index.hu/mindekozben/poszt/2017/10/20/art_deco_budapest_varosnezo_zsebkonyv_bolla_zoltan/', + 'body' => array( + '//div[@class="mindenkozben_post_content content"]', + '//div[@id="content"]' + ), + 'strip' => array( + '//div[@class="topszponzor_wrapper"]', + '//ul[@class="cikk-cimkek"]', + '//div[@class="author-share-date-container"]', + '//div[@class="pp-list"]', + '//div[@class="social-stripe cikk-bottom-box"]', + '//div[@class="cikk-bottom-text-ad"]', + '//a[@name="hozzaszolasok"]', + '//div[@class="cikk-vegi-ajanlo-reklamok-container"]', + '//div[@id="comments"]', + '//div[@class="comments"]', + '//div[@class="linkpreview-box bekezdes_utan"]', + '//div[@class="lapozo"]', + '//div[@class="szelso-jobb"]', + '//div[@class="social cikk-bottom-box"]', + '//input' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php new file mode 100644 index 0000000..a40ce69 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://indiehaven.com/no-mans-sky-is-a-solo-space-adventure-and-im-ok-with-that/', + 'body' => array( + '//section[contains(@class, "entry-content")]', + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php new file mode 100644 index 0000000..569013d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://inforadio.hu/belfold/2017/10/20/fontos_valtozas_vegleg_lezarnak_tobb_villamosatjarot_budapesten/', + 'body' => array( + '//div[@class="content-title"]', + '//div[@class="szelso-jobb-lead_container"]', + '//div[@class="cikk-torzs"]' + ), + 'strip' => array( + '//div[@id="microsite_microsite"]', + '//div[@class="cikk-bottom-text-ad"]', + '//div[@class="social-stripe_container"]', + '//div[@class="facebook-like-box"]', + '//div[@class="rovat sargabg rovatdobozcim"]', + '//div[@class="m-okosradio_magazin arenaMagazineItem"]', + '//header[@class="m-okosradio_header"]', + '//div[@class="m-okosradio_elo"]', + '//div[@class="m-okosradio_container"]', + '//form' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php new file mode 100644 index 0000000..5a021a0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://ing.dk/artikel/smart-husisolering-og-styring-skal-mindske-japans-energikrise-164517', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php new file mode 100644 index 0000000..90f8759 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%()%' => '$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php new file mode 100644 index 0000000..af99fe9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'body' => array('//span[@class="ccbnTxt"]'), + 'strip' => array(), + 'test_url' => 'http://ir.amd.com/phoenix.zhtml?c=74093&p=RssLanding&cat=news&id=2055819', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php new file mode 100644 index 0000000..9959441 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.japantimes.co.jp/news/2015/09/27/world/social-issues-world/pope-meets-sex-abuse-victims-philadelphia-promises-accountability/', + 'body' => array( + '//article[@role="main"]', + ), + 'strip' => array( + '//script', + '//header', + '//div[contains(@class, "meta")]', + '//div[@class="clearfix"]', + '//div[@class="OUTBRAIN"]', + '//ul[@id="content_footer_menu"]', + '//div[@class="article_footer_ad"]', + '//div[@id="disqus_thread"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php new file mode 100644 index 0000000..22485d6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.japantoday.com/category/politics/view/japan-u-s-to-sign-new-base-environment-pact', + 'body' => array( + '//div[@id="article_container"]', + ), + 'strip' => array( + '//h2', + '//div[@id="article_info"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php new file mode 100644 index 0000000..876b269 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www./2014/05/20/le-playstation-now-arrive-en-beta-fermee-aux-etats-unis/', + 'body' => array( + '//div[@class="post-content"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php new file mode 100644 index 0000000..5895256 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php @@ -0,0 +1,37 @@ + array( + '%.%/picture-gallery/%' => array( + 'test_url' => 'http://www.jsonline.com/picture-gallery/news/local/milwaukee/2017/02/22/photos-aclu-sues-milwaukee-police-over-profiling-stop-and-frisk/98250836/', + 'body' => array( + '//div[@class="priority-asset-gallery galleries standalone hasendslate"]', + ), + 'strip' => array( + '//div[@class="buy-photo-btn"]', + '//div[@class="gallery-thumbs thumbs pag-thumbs")]', + ), + ), + '%.*%' => array( + 'test_url' => 'http://www.jsonline.com/news/usandworld/as-many-as-a-million-expected-for-popes-last-mass-in-us-b99585180z1-329688131.html', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//p/span', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php new file mode 100644 index 0000000..089ff29 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://justcoolidea.ru/idealnyj-sad-samodelnye-proekty-dlya-berezhlivogo-domovladeltsa/', + 'body' => array( + '//section[@class="entry-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[contains(@class, "essb_links")]', + '//*[contains(@rel, "nofollow")]', + '//*[contains(@class, "ads")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php new file mode 100644 index 0000000..c3a1abc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.kanpai.fr/japon/comment-donner-lheure-en-japonais.html', + 'body' => array( + '//div[@class="single-left"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php new file mode 100644 index 0000000..25d6dfa --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://karriere.jobfinder.dk/artikel/dansk-professor-skal-lede-smart-grid-forskning-20-millioner-dollars-763', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php new file mode 100644 index 0000000..7568901 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.kisalfold.hu/szorakozas/egy_15_eves_srac_szuntetheti_meg_a_wc-parat_budapesten/2536699/', + 'body' => array( + '//header[@class="single-article__header"]/h1', + '//header[@class="single-article__header"]/h2', + '//figure[@class="single-article__image"]/img', + '//div[@class="single-article__content"]/div[@id="single-article__lead"]', + '//div[@id="article_text"]' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php new file mode 100644 index 0000000..c44a08f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://kiszamolo.hu/30-eve-volt-a-fekete-hetfo/', + 'body' => array( + '//article/h2', + '//article/div[@class="entry clearfix"]/p' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php new file mode 100644 index 0000000..439fc90 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'https://kodi.tv/article/andwere-baaaaack', + 'body' => array( + '//div[@class="l-region--content"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php new file mode 100644 index 0000000..9651056 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.koreaherald.com/view.php?ud=20150926000018', + 'body' => array( + '//div[@id="articleText"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php new file mode 100644 index 0000000..f274b4a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.koreatimes.co.kr/www/news/nation/2015/12/116_192409.html', + 'body' => array( + '//div[@id="p"]', + ), + 'strip' => array( + '//div[@id="webtalks_btn_listenDiv"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php new file mode 100644 index 0000000..12697cc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php new file mode 100644 index 0000000..e6aae46 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => array( + 'http://www.legorafi.fr/2016/12/16/gorafi-magazine-bravo-vous-avez-bientot-presque-survecu-a-2016/', + 'http://www.legorafi.fr/2016/12/15/manuel-valls-promet-quune-fois-elu-il-debarrassera-la-france-de-manuel-valls/', + ), + 'body' => array( + '//section[@id="banner_magazine"]', + '//figure[@class="main_picture"]', + '//div[@class="content"]', + ), + 'strip' => array( + '//figcaption', + '//div[@class="sharebox"]', + '//div[@class="tags"]', + '//section[@class="taboola_article"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php new file mode 100644 index 0000000..8f2b293 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lejapon.fr/guide-voyage-japon/5223/tokyo-sous-la-neige.htm', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[contains(@class, "addthis_toolbox")]', + '//*[contains(@class, "addthis_default_style")]', + '//*[@class="navigation small"]', + '//*[@id="related"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php new file mode 100644 index 0000000..369206a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lesjoiesducode.fr/post/75576211207/quand-lappli-ne-fonctionne-plus-sans-aucune-raison', + 'body' => array( + '//div[@class="blog-post-content"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php new file mode 100644 index 0000000..d978a5f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lfg.co/page/871/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+LookingForGroup+%28Looking+For+Group%29&utm_content=FeedBurner', + 'body' => array( + '//*[@id="comic"]/img | //*[@class="content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php new file mode 100644 index 0000000..b9a6933 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lifehacker.com/bring-water-bottle-caps-into-concerts-to-protect-your-d-1269334973', + 'body' => array( + '//div[contains(@class, "row")/img', + '//div[contains(@class, "content-column")]', + ), + 'strip' => array( + '//*[contains(@class, "meta")]', + '//span[contains(@class, "icon")]', + '//h1', + '//aside', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php new file mode 100644 index 0000000..bc140f6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lifehacker.ru/2016/03/03/polymail/', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="wp-thumbnail-caption"]', + '//*[contains(@class, "social-likes")]', + '//*[@class="jp-relatedposts"]', + '//*[contains(@class, "wpappbox")]', + '//*[contains(@class, "icon__image")]', + '//div[@id="hypercomments_widget"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php new file mode 100644 index 0000000..f4bc07d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux-magazin.de/Ausgaben/2017/09/AWS-Alternativen', + 'body' => array( + '//div[@class="attribute-content"]/div[@class="attribute-intro"]', + '(//div[@class="attribute-image"])[1]', + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//p[@class="attribute-advice"]', + ) + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php new file mode 100644 index 0000000..2520d0d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux.org/threads/lua-the-scripting-interpreter.8352/', + 'body' => array( + '//div[@class="messageContent"]', + ), + 'strip' => array( + '//aside', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php new file mode 100644 index 0000000..7fa0249 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux.org/threads/lua-the-scripting-interpreter.8352/', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php new file mode 100644 index 0000000..4e0a4cc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linuxinsider.com/story/82526.html?rss=1', + 'body' => array( + '//div[@id="story"]', + ), + 'strip' => array( + '//script', + '//h1', + '//div[@id="story-toolbox1"]', + '//div[@id="story-byline"]', + '//div[@id="story"]/p', + '//div[@class="story-advertisement"]', + '//iframe', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php new file mode 100644 index 0000000..c7051a2 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lists.freebsd.org/pipermail/freebsd-announce/2013-September/001504.html', + 'body' => array( + '//pre', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php new file mode 100644 index 0000000..d358e15 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://loldwell.com/?comic=food-math-101', + 'body' => array('//*[@id="comic"]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php new file mode 100644 index 0000000..816233d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'body' => array('//div[@id="comic"]//img'), + 'strip' => array(), + 'test_url' => 'http://www.lukesurl.com/archives/comic/665-3-of-clubs', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php new file mode 100644 index 0000000..bbe6dbc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.macg.co//logiciels/2014/05/feedly-sameliore-un-petit-peu-sur-mac-82205', + 'body' => array( + '//div[contains(@class, "field-name-body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php new file mode 100644 index 0000000..bca347e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.maclife.de/news/neue-farbe-iphone-8-kommt-blush-gold-10094817.html', + 'body' => array( + '//div[contains(@class, "article_wrapper")]/p | //div[contains(@class, "article_wrapper")]/h2 | //div[@class="gallery"]//figure | //div[contains(@class, "gallery_single")]//figure', + ) + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php new file mode 100644 index 0000000..32d78bc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.magyarkurir.hu/hirek/a-vilagszerte-ismert-dicsoito-csapat-hillsong-young-free-lep-fel-budapesten', + 'body' => array( + '//div[@class="behuzas"]' + ), + 'strip' => array( + '//div[@class="ikonsav"]', + '//p[@class="copyright"]', + '//div[@class="cimkek"]', + '//div[@id="footerbanner"]', + '//div[@class="rovat sargabg rovatdobozcim"]', + '//div[@class="rovatdoboz"]', + '//a[contains(., "Own")]', + '//a[@class="fblink"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php new file mode 100644 index 0000000..5f582a6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://marc.info/?l=openbsd-misc&m=141987113202061&w=2', + 'body' => array( + '//pre', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php new file mode 100644 index 0000000..469640d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.marriedtothesea.com/index.php?date=052915', + 'body' => array( + '//div[@align]/a/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php new file mode 100644 index 0000000..b8665e3 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="cc-comic"]', + '//div[@class="cc-newsbody"]', + ), + 'strip' => array(), + 'test_url' => 'http://www.marycagle.com/letsspeakenglish/74-grim-reality/', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php new file mode 100644 index 0000000..8880054 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://maximumble.thebookofbiff.com/2015/04/20/1084-change/', + 'body' => array('//div[@id="comic"]/div/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php new file mode 100644 index 0000000..e20860e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'https://medium.com/lessons-learned/917b8b63ae3e', + 'body' => array( + '//div[@class="section-content"]', + ), + 'strip' => array( + '//div[contains(@class,"metabar")]', + '//img[contains(@class,"thumbnail")]', + '//h1', + '//blockquote', + '//div[@class="aspectRatioPlaceholder-fill"]', + '//footer' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php new file mode 100644 index 0000000..c7a27de --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'body' => array('//div[@id="comic"]', + '//div[contains(@class,"entry-content")]', + ), + 'strip' => array(), + 'test_url' => 'http://mercworks.net/comicland/healthy-choice/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php new file mode 100644 index 0000000..5011169 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.metronieuws.nl/sport/2015/04/broer-fellaini-zorgde-bijna-voor-paniek-bij-mourinho', + 'body' => array('//div[contains(@class,"article-top")]/div[contains(@class,"image-component")] | //div[@class="article-full-width"]/div[1]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php new file mode 100644 index 0000000..ddb29a5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://milwaukeenns.org/2016/01/08/united-way-grant-enables-sdc-to-restore-free-tax-assistance-program/', + 'body' => array( + '//div[@class="pf-content"]', + ), + 'strip' => array( + '//div[@class="printfriendly"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php new file mode 100644 index 0000000..a2799b8 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://mno.hu/kulfold/elnokot-valasztanak-szloveniaban-2422840', + 'body' => array( + '//div[@class="header"]/h1', + '//div[@class="content hircikk clearfix"]/p' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php new file mode 100644 index 0000000..1ddcd40 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://mokepon.smackjeeves.com/comics/2120096/chapter-9-page-68/', + 'body' => array('//*[@id="comic_area_inner"]/img | //*[@id="comic_area_inner"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php new file mode 100644 index 0000000..f87560e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monandroid.com/blog/tutoriel-avance-activer-le-stockage-fusionne-sur-android-6-marshamallow-t12.html', + 'body' => array( + '//div[@class="blog-post-body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php new file mode 100644 index 0000000..b2b24d7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monwindows.com/tout-savoir-sur-le-centre-d-action-de-windows-phone-8-1-t40574.html', + 'body' => array( + '//div[@class="blog-post-body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php new file mode 100644 index 0000000..dd84284 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.moya-planeta.ru/travel/view/chto_yaponcu_horosho_russkomu_ne_ponyat_20432/', + 'body' => array( + '//div[@class="full_object"]', + ), + 'strip' => array( + '//div[@class="full_object_panel object_panel"]', + '//div[@class="full_object_panel_geo object_panel"]', + '//div[@class="full_object_title"]', + '//div[@class="full_object_social_likes"]', + '//div[@class="full_object_planeta_likes"]', + '//div[@class="full_object_go2comments"]', + '//div[@id="yandex_ad_R-163191-3"]', + '//div[@class="full_object_shop_article_recommend"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php new file mode 100644 index 0000000..b971091 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%alt="(.+)" */>%' => '/>
$1', + '%\.png%' => '_rollover.png', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php new file mode 100644 index 0000000..9e354a3 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.muckrock.com/news/archives/2016/jan/13/5-concerns-private-prisons/', + 'body' => array( + '//div[@class="content"]', + ), + 'strip' => array( + '//div[@class="newsletter-widget"]', + '//div[@class="contributors"]', + '//time', + '//h1', + '//div[@class="secondary"]', + '//aside', + '//div[@class="articles__related"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php new file mode 100644 index 0000000..b630915 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.mynorthshorenow.com/story/news/local/fox-point/2017/04/04/fox-point-building-board-approves-dunwood-commons-project/99875570/', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//p/span', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php new file mode 100644 index 0000000..ec2d5fd --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://feedproxy.google.com/~r/NakedCapitalism/~3/JOBxEHxN8ZI/mark-blyth-liberalism-undermined-democracy-failure-democratic-party.html', + 'body' => array( + '//div[@class="pf-content"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php new file mode 100644 index 0000000..c6692d0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.nasa.gov/image-feature/jpl/pia20514/coy-dione', + 'body' => array( + '//div[@class="article-body"]', + ), + 'strip' => array( + '//div[@class="title-bar"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php new file mode 100644 index 0000000..1a42d99 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nat-geo.ru/fact/868093-knidos-antichnyy-naukograd/', + 'body' => array( + '//div[@class="article-inner-text"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php new file mode 100644 index 0000000..5e612be --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nationaljournal.com/s/354962/south-carolina-evangelicals-outstrip-establishment?mref=home_top_main', + 'body' => array( + '//div[@class="section-body"]', + ), + 'strip' => array( + '//*[contains(@class, "-related")]', + '//*[contains(@class, "social")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php new file mode 100644 index 0000000..6b9e87f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nature.com/doifinder/10.1038/nature.2015.18340', + 'body' => array( + '//div[contains(@class,"main-content")]', + ), + 'strip' => array(), + ), + ), +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php new file mode 100644 index 0000000..c8ea926 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nba.com/2015/news/09/25/knicks-jackson-to-spend-more-time-around-coaching-staff.ap/index.html?rss=true', + 'body' => array( + '//div[@class="paragraphs"]', + ), + 'strip' => array( + '//div[@id="nbaArticleSocialWrapper_bot"]', + '//h5', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php new file mode 100644 index 0000000..1852435 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.networkworld.com/article/3020585/security/the-incident-response-fab-five.html', + 'body' => array( + '//figure/img[@class="hero-img"]', + '//section[@class="deck"]', + '//div[@itemprop="articleBody"] | //div[@itemprop="reviewBody"]', + '//div[@class="carousel-inside-crop"]', + ), + 'strip' => array( + '//script', + '//aside', + '//div[@class="credit"]', + '//div[@class="view-large"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php new file mode 100644 index 0000000..e0c0d19 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.neustadt-ticker.de/41302/alltag/kultur/demo-auf-der-boehmischen', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "sharedaddy")]', + '//*[contains(@class, "yarpp-related")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php new file mode 100644 index 0000000..29dd9d6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nextinpact.com/news/101122-3d-nand-intel-lance-six-nouvelles-gammes-ssd-pour-tous-usages.htm', + 'body' => array( + '//div[@class="container_article"]', + ), + 'strip' => array( + '//div[@class="infos_article"]', + '//div[@id="actu_auteur"]', + '//div[@id="soutenir_journaliste"]', + '//section[@id="bandeau_abonnez_vous"]', + '//br' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php new file mode 100644 index 0000000..f41e443 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php @@ -0,0 +1,10 @@ + array( + '%/archives.*%' => array( + 'test_url' => 'http://niceteethcomic.com/archives/page119/', + 'body' => array('//*[@class="comicpane"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php new file mode 100644 index 0000000..4d083f9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%.*static.nichtlustig.de/comics/full/(\\d+).*%s' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php new file mode 100644 index 0000000..b85e6b2 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nlcafe.hu/ezvan/20171021/nyugdijas-drogdilert-fogtak-a-ferencvarosi-rendorok/', + 'body' => array( + '//div[@class="single-title"]', + '//div[@class="single-excerpt"]', + '//div[@class="single-post-container-content"]/p', + '//div[@class="single-post-container-content"]/div' + ), + 'strip' => array( + '//div[@class="widget-container related-articles bigdata-widget related-full"]', + '//div[@class="banner-container clear-banner-row clearfix"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php new file mode 100644 index 0000000..cef3595 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.novo-argumente.com/artikel/der_kampf_gegen_die_schlafkrankheit', + 'body' => array( + '//main/div/article', + ), + 'strip' => array( + '//*[@class="artikel-datum"]', + '//*[@class="artikel-titel"]', + '//*[@class="artikel-autor"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php new file mode 100644 index 0000000..8b2b5b6 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="strip"]', + '//a/div[@id="nx"]/..', + ), + 'strip' => array(), + 'test_url' => 'http://oglaf.com/slodging/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%alt="(.+)" title="(.+)" */>%' => '/>
$1
$2
', + '%%' => 'Next page', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php new file mode 100644 index 0000000..213849d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://onhax.net/process-lasso-8-9-1-4-pro-key-portable-is-here-latest', + 'body' => array( + '//div[@class="postcontent"]', + ), + 'strip' => array( + '//*[@class="sharedaddy sd-sharing-enabled"]', + '//*[@class="yarpp-related"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php new file mode 100644 index 0000000..7382612 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.onlinekosten.de/news/android-8-0-die-neuen-features-im-ueberblick_209619.html?utm_source=rss&utm_medium=feed&utm_campaign=android-8-0-die-neuen-features-im-ueberblick', + 'body' => array( + '//p[@class="cms-widget_article_lead"]', + '//img[@class="bec_img"]', + '//div[@class="cms-widget_article_body"]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php new file mode 100644 index 0000000..f66ac4b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://onmilwaukee.com/movies/articles/downerspelunking.html', + 'body' => array( + '//article[contains(@class, "show")]', + ), + 'strip' => array( + '//h1', + '//div[contains(@class,"-ad")]', + '//div[contains(@class,"_ad")]', + '//div[@id="pub_wrapper"]', + '//div[contains(@class,"share_tools")]', + '//div[@class="clearfix"]', + '//div[contains(@class,"image_control")]', + '//section[@class="ribboned"]', + '//div[contains(@class,"sidebar")]', + '//aside[@class="article_tag_list"]', + '//section[contains(@id,"more_posts")]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php new file mode 100644 index 0000000..84f2bee --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.openculture.com/2017/03/are-we-living-inside-a-computer-simulation-watch-the-simulation-argument.html', + 'body' => array( + '//div[@class="entry"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php new file mode 100644 index 0000000..1fb7722 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.opennet.ru/opennews/art.shtml?num=46549', + 'body' => array( + '//*[@id="r_memo"]', + ), + 'strip' => array( + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php new file mode 100644 index 0000000..94139a7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.openrightsgroup.org/blog/2014/3-days-to-go-till-orgcon2014', + 'body' => array( + '//div[contains(@class, "content")]/div', + ), + 'strip' => array( + '//h2[1]', + '//div[@class="info"]', + '//div[@class="tags"]', + '//div[@class="comments"]', + '//div[@class="breadcrumbs"]', + '//h1[@class="pageTitle"]', + '//p[@class="bookmarkThis"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php new file mode 100644 index 0000000..60f3577 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://opensource.com/life/15/10/how-internet-things-will-change-way-we-think', + 'body' => array( + '//div[@id="article-template"]', + ), + 'strip' => array( + '//div[contains(@class,"os-article__sidebar")]', + '//div[@class="panel-pane pane-node-title"]', + '//div[@class="panel-pane pane-os-article-byline"]', + '//ul', + '//div[contains(@class,"-license")]', + '//div[contains(@class,"-tags")]', + '//div[@class="panel-pane pane-os-article-byline"]', + '//div[@class="os-article__content-below"]', + '//div[@id="comments"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php new file mode 100644 index 0000000..7bd9496 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.origo.hu/itthon/20171019-hamisan-tanuskodott-az-ugyved-ezert-nem-praktizalhat.html', + 'body' => array( + '//header[@id="article-head"]/h1', + '//article[@id="article-center"]' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php new file mode 100644 index 0000000..1d1396c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://osnews.com/story/28863/Google_said_to_be_under_US_antitrust_scrutiny_over_Android', + 'body' => array( + '//div[@class="newscontent1"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php new file mode 100644 index 0000000..b20bf41 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://pastebin.com/ed1pP9Ak', + 'body' => array( + '//div[@class="text"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php new file mode 100644 index 0000000..f180aee --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pcgameshardware.de/Dragon-Age-Thema-259929/News/Plaene-fuer-Teil-4-und-5-der-Serie-1235682/', + 'body' => array( + '//p[@class="introText"]', + '//figure[contains(@class, "articleBigTeaser")]', + '//div[@id="articleTextBody"]//p | //div[@id="articleTextBody"]//h2[@class="anchorHeadline"]', + ), + 'strip' => array( + '//p[@class="introText"]//time', + ) + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php new file mode 100644 index 0000000..ce4891d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + // the extra space is required to strip the title cleanly + '%title="(.+) " */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php new file mode 100644 index 0000000..dd39983 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php @@ -0,0 +1,21 @@ + array( + '%/news/.*%' => array( + 'test_url' => 'http://penny-arcade.com/news/post/2015/04/15/101-part-two', + 'body' => array( + '//*[@class="postBody"]/*', + ), + 'strip' => array( + ), + ), + '%/comic/.*%' => array( + 'test_url' => 'http://penny-arcade.com/comic/2015/04/15', + 'body' => array( + '//*[@id="comicFrame"]/a/img', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php new file mode 100644 index 0000000..fa9052e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pixelbeat.org/programming/sigpipe_handling.html#1425573246', + 'body' => array( + '//div[@class="contentText"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php new file mode 100644 index 0000000..5e48a6c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'https://plus.google.com/+LarryPage/posts/Lh8SKC6sED1', + 'body' => array( + '//div[@role="article"]/div[contains(@class, "eE")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php new file mode 100644 index 0000000..801a281 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%( '$1$2$1bonus.png"/>', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php new file mode 100644 index 0000000..ad01dad --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.portfolio.hu/gazdasag/mennybe-vagy-pokolba-megy-ma-a-cseh-trump.265833.html', + 'body' => array( + '//div[@id="cikk"]/h1', + '//div[@class="smscontent"]' + ), + 'strip' => array( + '//div[@class="traderhirdetes ga_viewanalytics"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php new file mode 100644 index 0000000..bc76630 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.pro-linux.de/news/1/25252/chrome-62-erschienen.html', + 'body' => array( + '//div[@id="news"]', + ), + 'strip' => array( + '//h3[@class="topic"]', + '//h2[@class="title"]', + '//div[@class="picto"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php new file mode 100644 index 0000000..5dc8be8 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'https://publicpolicyforum.org/blog/going-extra-mile', + 'body' => array( + '//div[contains(@class,"field-name-post-date")]', + '//div[contains(@class,"field-name-body")]', + ), + 'strip' => array( + '//img[@src="http://publicpolicyforum.org/sites/default/files/logo3.jpg"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php new file mode 100644 index 0000000..bcfeeb9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.publy.ru/post/19988', + 'body' => array( + '//div[@class="singlepost"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="featured"]', + '//*[@class="toc_white no_bullets"]', + '//*[@class="toc_title"]', + '//*[@class="pba"]', + '//*[@class="comments"]', + '//*[contains(@class, "g-single")]', + '//*[@class="ts-fab-wrapper"]', + '//*[contains(@class, "wp_rp_wrap")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php new file mode 100644 index 0000000..9fa5568 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://putaindecode.fr/posts/js/etat-lieux-js-modulaire-front/', + 'body' => array( + '//*[@class="putainde-Post-md"]', + ), + 'strip' => array( + '//*[contains(@class, "inlineimg")]', + '//*[contains(@class, "comment-respond")]', + '//header', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php new file mode 100644 index 0000000..343cd12 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://recode.net/2015/09/26/big-tech-rolls-out-red-carpet-for-indian-prime-minister-lobbies-behind-closed-doors/', + 'body' => array( + '//img[contains(@class,"attachment-large")]', + '//div[contains(@class,"postarea")]', + '//li[@class,"author"]', + ), + 'strip' => array( + '//script', + '//div[contains(@class,"sharedaddy")]', + '//div[@class="post-send-off"]', + '//div[@class="large-12 columns"]', + '//div[contains(@class,"inner-related-article")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php new file mode 100644 index 0000000..b97c73e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://retractionwatch.com/2015/11/12/psychologist-jens-forster-settles-case-by-agreeing-to-2-retractions/', + 'body' => array( + '//*[@class="main"]', + '//*[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "sharedaddy")]', + '//*[contains(@class, "jp-relatedposts")]', + '//p[@class="p1"]', + ) + ) + ) +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php new file mode 100644 index 0000000..7111078 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.rockpapershotgun.com/2016/08/26/the-divisions-expansions-delayed-to-improve-the-game/', + 'body' => array( + '//div[@class="entry"]', + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php new file mode 100644 index 0000000..cb9116a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://rue89.feedsportal.com/c/33822/f/608948/s/30999fa0/sc/24/l/0L0Srue890N0C20A130C0A80C30A0Cfaisait0Eboris0Eboillon0Eex0Esarko0Eboy0E350A0E0A0A0A0Eeuros0Egare0Enord0E245315/story01.htm', + 'body' => array( + '//*[@id="article"]/div[contains(@class, "content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php new file mode 100644 index 0000000..9915c23 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.rugbyrama.fr/rugby/top-14/2015-2016/top-14-hayman-coupe-du-monde-finale-2012-lutte.-voici-levan-chilachava-toulon_sto5283863/story.shtml', + 'body' => array( + '//div[@class="storyfull__content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="share-buttons"]', + '//*[@class="ad"]', + '//*[@class="hide-desktop"]', + '//*[@id="tracking_img"]', + ) + ) + ) +); \ No newline at end of file diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com new file mode 100644 index 0000000..37f43e9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.salonkolumnisten.com/schulpolitik-niedersachsen/', + 'body' => array( + '//div[@id="main"]/div[contains(@class, "featimg")]', + '//div[@id="main"]/article/div[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//div[@id="main"]/article/div[contains(@class, "entry-content")]/a[1]', + '//div[@id="main"]/article/div[contains(@class, "entry-content")]/a[1]', + ), + ), + ), +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php new file mode 100644 index 0000000..d63fc11 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://satwcomic.com/day-at-the-beach', + 'body' => array( + '//div[@class="container"]/center/a/img', + '//span[@itemprop="articleBody"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php new file mode 100644 index 0000000..fcf045f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.science-skeptical.de/politik/diesel-die-lueckenmedien-im-glashaus-6/0016080/', + 'body' => array( + '//div[@class="pf-content"]', + ), + 'strip' => array( + '//div[contains(@class, "printfriendly")]', + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php new file mode 100644 index 0000000..7835fd9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.scrumalliance.org/community/articles/2015/march/an-introduction-to-agile-project-intake?feed=articles', + 'body' => array( + '//div[@class="article_content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php new file mode 100644 index 0000000..0104514 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.securityfocus.com/archive/1/540139', + 'body' => array( + '//div[@id="vulnerability"]', + '//div[@class="comments_reply"]', + ), + 'strip' => array( + '//span[@class="title"]', + '//div[@id="logo_new"]', + '//div[@id="bannerAd"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php new file mode 100644 index 0000000..f435417 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@class="comicpane"]/a/img', + '//div[@class="entry"]', + ), + 'strip' => array(), + 'test_url' => 'http://sentfromthemoon.com/archives/1417', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php new file mode 100644 index 0000000..ab0eb7d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.sitepoint.com/creating-hello-world-app-swift/', + 'body' => array( + '//section[@class="article_body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php new file mode 100644 index 0000000..89ced8b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://science.slashdot.org/story/15/04/20/0528253/pull-top-can-tabs-at-50-reach-historic-archaeological-status', + 'body' => array( + '//article/div[@class="body"] | //article[@class="layout-article"]/div[@class="elips"]', ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php new file mode 100644 index 0000000..8c13c44 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://smallhousebliss.com/2013/08/29/house-g-by-lode-architecture/', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "share")]', + '//*[contains(@class, "wpcnt")]', + '//*[contains(@class, "meta")]', + '//*[contains(@class, "postitle")]', + '//*[@id="nav-below"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php new file mode 100644 index 0000000..7463abc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://smarthomewelt.de/apple-tv-amazon-echo-smart-home/', + 'body' => array('//div[@class="entry-inner"]/p | //div[@class="entry-inner"]/div[contains(@class,"wp-caption")]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php new file mode 100644 index 0000000..cbe1072 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.smashingmagazine.com/2015/04/17/using-sketch-for-responsive-web-design-case-study/', + 'body' => array('//article[contains(@class,"post")]/p'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php new file mode 100644 index 0000000..42262dc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.smbc-comics.com/comic/the-troll-toll', + 'body' => array( + '//div[@id="cc-comicbody"]', + '//div[@id="aftercomic"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php new file mode 100644 index 0000000..b0fe655 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.snopes.com/bacca-brides-on-tour/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//span[@itemprop="author"]', + '//div[contains(@class,"author-")]', + '//h1', + '//style', + '//div[contains(@class,"socialShares")]', + '//div[contains(@class,"ad-unit")]', + '//aside', + '//div[contains(@class,"boomtrain")]', + '//footer' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php new file mode 100644 index 0000000..6448bb0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.soundandvision.com/content/james-guthrie-mixing-roger-waters-and-pink-floyd-51', + 'body' => array( + '//div[@id="left"]', + ), + 'strip' => array( + '//div[@class="meta"]', + '//div[@class="ratingsbox"]', + '//h1', + '//h2', + '//addthis', + '//comment-links', + '//div[@class="book-navigation"]', + '//div[@class="comment-links"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php new file mode 100644 index 0000000..b42c3aa --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.spiegel.de/politik/ausland/afrika-angola-geht-gegen-islam-vor-und-schliesst-moscheen-a-935788.html', + 'body' => array( + '//div[@class="spArticleContent"]/p | //div[@class="spArticleContent"]//img[@class="spResponsiveImage "]', + ), + 'strip' => array( + '//div[@class="author-details"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php new file mode 100644 index 0000000..8e410be --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.stereophile.com/content/2015-rocky-mountain-audio-fest-starts-friday', + 'body' => array( + '//div[@class="content clear-block"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php new file mode 100644 index 0000000..61182d7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://stupidfox.net/134-sleepy-time', + 'body' => array( + '//div[@class="comicmid"]/center/a/img', + '//div[@class="stand_high"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php new file mode 100644 index 0000000..6d74427 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.subtraction.com/2015/06/06/time-lapse-video-of-one-world-trade-center/', + 'body' => array('//article/div[@class="entry-content"]'), + 'strip' => array(), + ), + ), + 'filter' => array( + '%.*%' => array( + '%\+%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php new file mode 100644 index 0000000..90bde5a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://sz.de/1.2443161', + 'body' => array('//article[@id="sitecontent"]/section[@class="topenrichment"]//img | //article[@id="sitecontent"]/section[@class="body"]/section[@class="authors"]/preceding-sibling::*[not(contains(@class, "ad"))]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php new file mode 100644 index 0000000..624ef90 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://takprosto.cc/kokteyl-dlya-pohudeniya-v-domashnih-usloviyah/', + 'body' => array( + '//div[contains(@class, "entry-contentt")]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="views_post"]', + '//*[contains(@class, "mailchimp-box")]', + '//*[contains(@class, "essb_links")]', + '//*[contains(@rel, "nofollow")]', + '//*[contains(@class, "ads")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php new file mode 100644 index 0000000..230b791 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://techcrunch.com/2013/08/31/indias-visa-maze/', + 'body' => array( + '//div[contains(@class, "media-container")]', + '//div[contains(@class, "article-entry")]', + ), + 'strip' => array( + '//*[contains(@class, "module-crunchbase")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php new file mode 100644 index 0000000..3b01eb9 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.the-ebook-reader.com/2015/09/25/kobo-glo-hd-and-kobo-touch-2-0-covers-and-cases-roundup/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//div[@id="share"]', + '//div[contains(@class,"ItemCenter")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php new file mode 100644 index 0000000..bfad4ab --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theatlantic.com/politics/archive/2015/09/what-does-it-mean-to-lament-the-poor-inside-panem/407317/', + 'body' => array( + '//picture[@class="img"]', + '//figure/figcaption/span', + '//div/p[@itemprop="description"]', + '//div[@class="article-body"]', + '//ul[@class="photos"]', + ), + 'strip' => array( + '//aside[@class="callout"]', + '//span[@class="credit"]', + '//figcaption[@class="credit"]', + '//aside[contains(@class,"partner-box")]', + '//div[contains(@class,"ad")]', + '//a[contains(@class,"social-icon")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php new file mode 100644 index 0000000..fd4f3d5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php @@ -0,0 +1,12 @@ + array( + '%/comic/.*%' => array( + 'test_url' => 'http://theawkwardyeti.com/comic/things-to-do/', + 'body' => array( + '//div[@id="comic"]' + ), + 'strip' => array() + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php new file mode 100644 index 0000000..c6ec5bf --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://thecodinglove.com/post/116897934767', + 'body' => array('//div[@class="bodytype"]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php new file mode 100644 index 0000000..d2f840d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@class="comicpane"]/a/img', + '//div[@class="entry"]', + ), + 'strip' => array(), + 'test_url' => 'http://thedoghousediaries.com/6023', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php new file mode 100644 index 0000000..f9b4637 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thegamercat.com/comic/just-no/', + 'body' => array('//div[@id="comic"] | //div[@class="post-content"]/div[@class="entry"]/p'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php new file mode 100644 index 0000000..1e6735b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thehindu.com/sci-tech/science/why-is-the-shape-of-cells-in-a-honeycomb-always-hexagonal/article7692306.ece?utm_source=RSS_Feed&utm_medium=RSS&utm_campaign=RSS_Syndication', + 'body' => array( + '//div/img[@class="main-image"]', + '//div[@class="photo-caption"]', + '//div[@class="articleLead"]', + '//p', + '//span[@class="upper"]', + ), + 'strip' => array( + '//div[@id="articleKeywords"]', + '//div[@class="photo-source"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php new file mode 100644 index 0000000..c3ec250 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'www.thelocal.se/20161219/this-swede-can-memorize-hundreds-of-numbers-in-only-five-minutes', + 'body' => array( + '//div[@id="article-photo"]', + '//div[@id="article-description"]', + '//div[@id="article-body"]', + ), + 'strip' => array( + '//div[@id="article-info-middle"]', + ) + ) + ) +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php new file mode 100644 index 0000000..bc47b27 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.themerepublic.net/2015/04/david-lopez-pitoko.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+blogspot%2FDngUJ+%28Theme+Republic%29&utm_content=FeedBurner', + 'body' => array('//*[@class="post-body"]'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php new file mode 100644 index 0000000..0f5bf75 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.themoscowtimes.com/business/article/535500.html', + 'body' => array( + '//div[@class="article_main_img"]', + '//div[@class="article_text"]', + ), + 'strip' => array( + '//div[@class="articlebottom"]', + '//p/b', + '//p/a[contains(@href, "/article.php?id=")]', + '//div[@class="disqus_wrap"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php new file mode 100644 index 0000000..7538170 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://international.thenewslens.com/post/255032/', + 'body' => array( + '//div[@class="article-section"]', + ), + 'strip' => array( + '//div[contains(@class,"ad-")]', + '//div[@class="article-title-box"]', + '//div[@class="function-box"]', + '//p/span', + '//aside', + '//footer', + '//div[@class="article-infoBot-box"]', + '//div[contains(@class,"standard-container")]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php new file mode 100644 index 0000000..acbfd36 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theonion.com/article/wild-eyed-jim-harbaugh-informs-players-they-must-k-51397?utm_medium=RSS&utm_campaign=feeds', + 'body' => array( + '//div[@class="content-masthead"]/figure/div/noscript/img', + '//div[@class="content-text"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php new file mode 100644 index 0000000..896365a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.theregister.co.uk/2017/10/21/purism_cleanses_laptops_of_intel_management_engine/', + 'body' => array( + '//div[@id="article"]', + ), + 'strip' => array( + '//div[@class="byline_and_share"]', + '//div[@class="social_btns alt_colour dcl"]', + '//div[@class="promo_article"]', + '//div[@id="article_body_btm"]', + '//p[@class="wptl btm"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php new file mode 100644 index 0000000..1163b34 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thestandard.com.hk/breaking_news_detail.asp?id=67156', + 'body' => array( + '//table/tr/td/span[@class="bodyCopy"]', + ), + 'strip' => array( + '//script', + '//br', + '//map[@name="gif_bar"]', + '//img[contains(@usemap,"gif_bar")]', + '//a', + '//span[@class="bodyHeadline"]', + '//i', + '//b', + '//table', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php new file mode 100644 index 0000000..09e876d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.theverge.com/2017/11/11/16624298/mindhunter-netflix-show-david-fincher-review', + 'body' => array( + '//figure[@class="e-image e-image--hero"]/span[@class="e-image__inner"]', + '//div[@class="c-entry-content"]', + ), + 'strip' => array( + '//div[@class="c-related-list"]', + '//div[@class="c-page-title"]', + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php new file mode 100644 index 0000000..4af6196 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="cc-comic"]', + ), + 'test_url' => 'http://www.threepanelsoul.com/comic/uncloaking', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php new file mode 100644 index 0000000..6fba3df --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.tichyseinblick.de/daili-es-sentials/jamaika-reaktionen-der-enttaeuschten/', + 'body' => array( + '//article' + ), + 'strip' => array( + '//header', + '//footer', + '//div[@class="mod-cad2"]', + '//ul[contains(@class, "social")]', + '//div[@class="rty-pop-up"]', + '//div[@class="pagelink"]', + '//div[@id="reward"]', + '//div[@class="rty-block-plista"]' + ) + ), + ), +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php new file mode 100644 index 0000000..1924680 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://timesofindia.indiatimes.com/city/mangaluru/Adani-UPCL-to-release-CSR-grant-of-Rs-3-74-crore-to-YellurGram-Panchayat/articleshow/50512116.cms', + 'body' => array( + '//div[@class="article_content clearfix"]', + '//section[@class="highlight clearfix"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php new file mode 100644 index 0000000..f2e009d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://totalcar.hu/tesztek/2017/10/21/veteran_fiat-abarth_1000tc_1968/', + 'body' => array( + '//div[@class="content-title"]', + '//div[@class="lead-container"]/div[@class="lead"]', + '//div[@class="cikk-torzs"]' + ), + 'strip' => array( + '//span[@class="gallery_title newline"]', + '//div[@class="social-stripe cikk-bottom-box"]', + '//div[@class="cikk-bottom-text-ad"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php new file mode 100644 index 0000000..97a0da0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.tozsdeforum.hu/szemelyes-penzugyek/napi-penzugyek/ezek-a-legnepszerubb-turistacelpontok-voltal-mar-mindenhol-87181.html', + 'body' => array( + '//header/h1', + '//div[@class="title_img"]', + '//article/div[@class="tf-post"]/div[@class="p"]/p|//article/div[@class="tf-post"]/div[@class="p"]/h3|//article/div[@class="tf-post"]/div[@class="p"]/blockquote' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php new file mode 100644 index 0000000..4ee4fcd --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php @@ -0,0 +1,15 @@ + array( + '%^/blog.*%' => array( + 'test_url' => 'http://travel-dealz.de/blog/venere-gutschein/', + 'body' => array('//div[@class="post-entry"]'), + 'strip' => array( + '//*[@id="jp-relatedposts"]', + '//*[@class="post-meta"]', + '//*[@class="post-data"]', + '//*[@id="author-meta"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php new file mode 100644 index 0000000..3ab8ca4 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://travelo.hu/csaladbarat/2017/10/20/mar_csak_egy_het_es_kezdodik_az_oszi_szunet/', + 'body' => array( + '//div[@id="content"]/h1', + '//div[@id="kopf"]', + '//div[@id="szoveg"]' + ), + 'strip' => array( + '//div[@class="goAdverticum"]', + '//h1[@class="border"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php new file mode 100644 index 0000000..55eb7e0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.treehugger.com/uncategorized/top-ten-posts-week-bunnies-2.html', + 'body' => array( + '//div[contains(@class, "promo-image")]', + '//div[contains(@id, "entry-body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php new file mode 100644 index 0000000..7e39145 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://tutorialzine.com/2017/10/15-interesting-javascript-and-css-libraries-for-october-2017', + 'body' => array( + '//article' + ), + 'strip' => array( + '//div[@class="article__header"]', + '//div[@class="article__share"]', + '//div[@class="article__footer"]', + '//div[@id="article__related-articles"]', + '//div[@class="webappstudio-animation"]', + '//div[@class="ad-container adsbygoogle hidden-xs hidden-sm"]', + '//script' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php new file mode 100644 index 0000000..79f4f62 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%http://www.twogag.com/comics-rss/([^.]+)\\.jpg%' => 'http://www.twogag.com/comics/$1.jpg', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php new file mode 100644 index 0000000..3428fcb --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://twokinds.keenspot.com/archive.php?p=0', + 'body' => array('//*[@class="comic"]/div/a/img | //*[@class="comic"]/div/img | //*[@id="cg_img"]/img | //*[@id="cg_img"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php new file mode 100644 index 0000000..8b15900 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://undeadly.org/cgi?action=article&sid=20141101181155', + 'body' => array( + '/html/body/table[3]/tbody/tr/td[1]/table[2]/tr/td[1]', + ), + 'strip' => array( + '//font', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php new file mode 100644 index 0000000..ec8d1a1 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.upi.com/Top_News/US/2015/09/26/Tech-giants-Hollywood-stars-among-guests-at-state-dinner-for-Chinas-Xi-Jinping/4541443281006/', + 'body' => array( + '//div[@class="img"]', + '//div/article[@itemprop="articleBody"]', + ), + 'strip' => array( + '//div[@align="center"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php new file mode 100644 index 0000000..edd6aa4 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.usatoday.com/story/life/music/2017/02/13/things-you-should-know-happened-grammy-awards-2017/97833734/', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//script', + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php new file mode 100644 index 0000000..a6d49f2 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.version2.dk/artikel/surface-pro-2-fungerer-bedre-til-arbejde-end-fornoejelse-55195', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php new file mode 100644 index 0000000..acd68c0 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.vezess.hu/hirek/2017/10/20/audi-a7-2018-bemutato/', + 'body' => array( + '//article[@id="news"]/h1', + '//article[@id="news"]/h2', + '//article[@id="news"]/p[@class="lead"]', + '//article[@id="news"]/p[@class="main-pic responsive-img-container"]', + '//div[@class="article-body"]' + ), + 'strip' => array( + '//div[@class="info-bar"]', + '//ul[@class="breadcrumb"]', + '//div[@class="embed-link ce_widget"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php new file mode 100644 index 0000000..b2830a3 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php @@ -0,0 +1,15 @@ + array( + '%/comics.*%' => array( + 'test_url' => 'http://www.vgcats.com/comics/?strip_id=358', + 'body' => array('//*[@align="center"]/img'), + 'strip' => array(), + ), + '%/super.*%' => array( + 'test_url' => 'http://www.vgcats.com/super/?strip_id=84', + 'body' => array('//*[@align="center"]/p/img'), + 'strip' => array(), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php new file mode 100644 index 0000000..b9bef7a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.vuxml.org/freebsd/a5f160fa-deee-11e4-99f8-080027ef73ec.html', + 'body' => array( + '//body', + ), + 'strip' => array( + '//h1', + '//div[@class="blurb"]', + '//hr', + '//p[@class="copyright"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php new file mode 100644 index 0000000..58aceea --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.wausaudailyherald.com/story/news/2017/04/01/hundreds-gather-remember-attorney-killed-shooting-spree/99826062/?from=global&sessionKey=&autologin=', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//p/span', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php new file mode 100644 index 0000000..83a2ebe --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.welt.de/debatte/kommentare/article169740590/Bloss-keine-sozialdemokratische-Konsenssause.html', + 'body' => array( + '//main/article/header/div/div[contains(@class, "c-summary")]/div', + '//main/article/header/div[3]/div/figure/div/div/div/picture[1]', + '//main/article/header/div[3]/div/figure/figcaption/child::*', + '//main/article/div[contains(@class, "c-article-text")]' + ), + 'strip' => array( + '//*[contains(@class, "c-inline-element--has-commercials")]', + '//*[contains(@class, "c-inline-teaser")]', + '//figure[contains(@class, "c-video-element")]', + '//main/article/div[contains(@class, "c-article-text")]/div[@class="c-inline-element"]/div[contains(@class, "c-image-element")]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php new file mode 100644 index 0000000..adcf417 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.westfalen-blatt.de/OWL/Lokales/Kreis-Hoexter/Warburg/3024113-Polizei-in-Warburg-Hier-waren-keine-kriminellen-Profis-am-Werk-Wurstautomat-Sprengung-mit-Polen-Boellern', + 'body' => array( + '//div[contains(@class, "articleimage")]', + '//div[@class="attribute-short"]', + '//div[@class="attribute-long"]', + ), + 'strip' => array( + '//div[@class="fb-post"]' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php new file mode 100644 index 0000000..98fc368 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php @@ -0,0 +1,33 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bbc.co.uk/news/world-middle-east-23911833', + 'body' => array( + '//div[@class="story-body__inner"] | //div[@class="article"]', + '//div[@class="indPost"]', + ), + 'strip' => array( + '//form', + '//div[@id="headline"]', + '//*[@class="warning"]', + '//span[@class="off-screen"]', + '//span[@class="story-image-copyright"]', + '//ul[@class="story-body__unordered-list"]', + '//div[@class="ad_wrapper"]', + '//div[@id="article-sidebar"]', + '//div[@class="data-table-outer"]', + '//*[@class="story-date"]', + '//*[@class="story-header"]', + '//figure[contains(@class,"has-caption")]', + '//*[@class="story-related"]', + '//*[contains(@class, "byline")]', + '//p[contains(@class, "media-message")]', + '//*[contains(@class, "story-feature")]', + '//*[@id="video-carousel-container"]', + '//*[@id="also-related-links"]', + '//*[contains(@class, "share") or contains(@class, "hidden") or contains(@class, "hyper")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php new file mode 100644 index 0000000..41ef68d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bdgest.com/chronique-6027-BD-Adrastee-Tome-2.html', + 'body' => array( + '//*[contains(@class, "chronique")]', + ), + 'strip' => array( + '//*[contains(@class, "post-review")]', + '//*[contains(@class, "footer-review")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php new file mode 100644 index 0000000..63ca069 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bgr.in/news/xiaomi-redmi-3-with-13-megapixel-camera-snapdragon-616-launched-price-specifications-and-features/', + 'body' => array( + '//div[@class="article-content"]', + ), + 'strip' => array( + '//*[@class="article-meta"]', + '//*[@class="contentAdsense300"]', + '//*[@class="iwpl-social-hide"]', + '//iframe[@class="iframeads"]', + '//*[@class="disqus_thread"]', + '//*[@class="outb-mobile OUTBRAIN"]', + '//*[@class="wdt_smart_alerts"]', + '//*[@class="footnote"]', + '//*[@id="gadget-widget"]', + '//header[@class="article-title entry-header"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php new file mode 100644 index 0000000..0acc44e --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.businessweek.com/articles/2013-09-18/elon-musks-hyperloop-will-work-says-some-very-smart-software', + 'body' => array( + '//div[@id="lead_graphic"]', + '//div[@id="article_body"]', + ), + 'strip' => array( + '//*[contains(@class, "related_item")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php new file mode 100644 index 0000000..31d03ed --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.cnn.com/2013/08/31/world/meast/syria-civil-war/index.html?hpt=hp_t1', + 'body' => array( + '//div[@class="cnn_strycntntlft"]', + ), + 'strip' => array( + '//div[@class="cnn_stryshrwdgtbtm"]', + '//div[@class="cnn_strybtmcntnt"]', + '//div[@class="cnn_strylftcntnt"]', + '//div[contains(@class, "cnnGalleryContainer")]', + '//div[contains(@class, "cnn_strylftcexpbx")]', + '//div[contains(@class, "articleGalleryNavContainer")]', + '//div[contains(@class, "cnnArticleGalleryCaptionControl")]', + '//div[contains(@class, "cnnArticleGalleryNavPrevNextDisabled")]', + '//div[contains(@class, "cnnArticleGalleryNavPrevNext")]', + '//div[contains(@class, "cnn_html_media_title_new")]', + '//div[contains(@id, "disqus")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php new file mode 100644 index 0000000..1535e43 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.developpez.com/actu/81757/Mozilla-annonce-la-disponibilite-de-Firefox-36-qui-passe-au-HTTP-2-et-permet-la-synchronisation-de-son-ecran-d-accueil/', + 'body' => array( + '//*[@itemprop="articleBody"]', + ), + 'strip' => array( + '//form', + '//div[@class="content"]/img', + '//a[last()]/following-sibling::*', + '//*[contains(@class,"actuTitle")]', + '//*[contains(@class,"date")]', + '//*[contains(@class,"inlineimg")]', + '//*[@id="signaler"]', + '//*[@id="signalerFrame"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php new file mode 100644 index 0000000..263f075 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.egscomics.com/index.php?id=1690', + 'title' => '/html/head/title', + 'body' => array( + '//img[@id="comic"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php new file mode 100644 index 0000000..c948c77 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fakingnews.firstpost.com/2016/01/engineering-student-creates-record-in-a-decade-becomes-the-first-to-completely-exhaust-ball-pen-refill/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[@class="socialshare_bar"]', + '//*[@class="authorbox"]', + '//*[@class="cf5_rps"]', + '//*[@class="60563 fb-comments fb-social-plugin"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php new file mode 100644 index 0000000..fd16ed5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.forbes.com/sites/andygreenberg/2013/09/05/follow-the-bitcoins-how-we-got-busted-buying-drugs-on-silk-roads-black-market/', + 'body' => array( + '//div[@id="leftRail"]/div[contains(@class, body)]', + ), + 'strip' => array( + '//aside', + '//div[contains(@class, "entity_block")]', + '//div[contains(@class, "vestpocket") and not contains(@class, "body")]', + '//div[contains(@style, "display")]', + '//div[contains(@id, "comment")]', + '//div[contains(@class, "widget")]', + '//div[contains(@class, "pagination")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php new file mode 100644 index 0000000..f7ec0d8 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.franceculture.fr/emission-culture-eco-la-finance-aime-toujours-la-france-2016-01-08', + 'body' => array( + '//div[@class="text-zone"]', + ), + 'strip' => array( + '//ul[@class="tags"]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php new file mode 100644 index 0000000..ea94a0f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.futura-sciences.com/magazines/espace/infos/actu/d/astronautique-curiosity-franchi-succes-dune-dingo-gap-52289/#xtor=RSS-8', + 'body' => array( + '//div[contains(@class, "content fiche-")]', + ), + 'strip' => array( + '//h1', + '//*[contains(@class, "content-date")]', + '//*[contains(@class, "diaporama")]', + '//*[contains(@class, "slider")]', + '//*[contains(@class, "cartouche")]', + '//*[contains(@class, "noprint")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php new file mode 100644 index 0000000..3d0b6c7 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.geekculture.com/joyoftech/joyarchives/2180.html', + 'body' => array( + '//p[contains(@class,"Maintext")][2]/a/img[contains(@src,"joyimages")]', + ), + 'strip' => array(), + ), + ), +); + diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php new file mode 100644 index 0000000..6879e76 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.howtogeek.com/235283/what-is-a-wireless-hard-drive-and-should-i-get-one/', + 'body' => array( + '//div[@class="thecontent"]', + ), + 'strip' => array( + '//*[@class="relatedside"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php new file mode 100644 index 0000000..dcb7e48 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lepoint.fr/c-est-arrive-aujourd-hui/19-septembre-1783-pour-la-premiere-fois-un-mouton-un-canard-et-un-coq-s-envoient-en-l-air-devant-louis-xvi-18-09-2012-1507704_494.php', + 'body' => array( + '//article', + ), + 'strip' => array( + '//*[contains(@class, "info_article")]', + '//*[contains(@class, "fildariane_titre")]', + '//*[contains(@class, "entete2_article")]', + '//*[contains(@class, "signature_article")]', + '//*[contains(@id, "share")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php new file mode 100644 index 0000000..0137e20 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lesnumeriques.com/blender/kitchenaid-diamond-5ksb1585-p27473/test.html', + 'body' => array( + '//*[@id="product-content"]', + '//*[@id="news-content"]', + '//*[@id="article-content"]', + ), + 'strip' => array( + '//form', + '//div[contains(@class, "price-v4"])', + '//div[contains(@class, "authors-and-date")]', + '//div[contains(@class, "mini-product")]', + '//div[@id="articles-related-authors"]', + '//div[@id="tags-socials"]', + '//div[@id="user-reviews"]', + '//div[@id="product-reviews"]', + '//div[@id="publication-breadcrumbs-and-date"]', + '//div[@id="publication-breadcrumbs-and-date"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php new file mode 100644 index 0000000..60bc1bd --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.mac4ever.com/actu/87392_video-quand-steve-jobs-et-bill-gates-jouaient-au-bachelor-avec-le-mac', + 'body' => array( + '//div[contains(@class, "news-news-content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php new file mode 100644 index 0000000..a274564 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.makeuseof.com/tag/having-problems-with-audio-in-windows-10-heres-a-likely-fix/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[@class="new_sharebar"]', + '//*[@class="author"]', + '//*[@class="wdt_grouvi"]', + '//*[@class="wdt_smart_alerts"]', + '//*[@class="modal fade grouvi"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php new file mode 100644 index 0000000..5f5e987 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monsieur-le-chien.fr/index.php?planche=672', + 'body' => array( + '//img[starts-with(@src, "i/planches/")]', + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php new file mode 100644 index 0000000..ecc0213 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php @@ -0,0 +1,28 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.npr.org/blogs/thesalt/2013/09/17/223345977/auto-brewery-syndrome-apparently-you-can-make-beer-in-your-gut', + 'body' => array( + '//article[contains(@class,"story")]', + ), + 'strip' => array( + '//div[@class="story-tools"]', + '//h3[@class="slug"]', + '//div[@class="storytitle"]', + '//div[@id="story-meta"]', + '//a[@id="mainContent"]', + '//div[@class="credit-caption"]', + '//div[@class="enlarge_html"]', + '//button', + '//div[contains(@id,"pullquote")]', + '//div[contains(@class,"internallink")]', + '//div[contains(@class,"video")]', + '//div[@class="simplenodate"]', + '//div[contains(@class,"share-")]', + '//div[@class="tags"]', + '//aside' + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php new file mode 100644 index 0000000..fe4971c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.numerama.com/sciences/125959-recherches-ladn-recompensees-nobel-de-chimie.html', + 'body' => array( + '//article', + ), + 'strip' => array( + '//footer', + '//section[@class="related-article"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php new file mode 100644 index 0000000..320c214 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.oneindia.com/india/b-luru-govt-likely-remove-word-eunuch-from-sec-36-a-karnataka-police-act-1981173.html', + 'body' => array( + '//div[@class="ecom-ad-content"]', + ), + 'strip' => array( + '//*[@id="view_cmtns"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php new file mode 100644 index 0000000..9e467ed --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pseudo-sciences.org/spip.php?article2275', + 'body' => array( + '//div[@id="art_main"]', + ), + 'strip' => array( + '//div[@id="art_print"]', + '//div[@id="art_chapo"]', + '//img[@class="puce"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php new file mode 100644 index 0000000..ae7a93a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.sciencemag.org/news/2016/01/could-bright-foamy-wak$', + 'body' => array( + '//div[@class="row--hero"]', + '//article[contains(@class,"primary")]', + ), + 'strip' => array( + '//header[@class="article__header"]', + '//footer[@class="article__foot"]', + ), + ), + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php new file mode 100644 index 0000000..8c8dc89 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.slate.fr/monde/77034/allemagne-2013-couacs-campagne', + 'body' => array( + '//div[@class="article_content"]', + ), + 'strip' => array( + '//*[@id="slate_associated_bn"]', + '//*[@id="ligatus-article"]', + '//*[@id="article_sidebar"]', + '//div[contains(@id, "reseaux")]', + '//*[contains(@class, "smart") or contains(@class, "article_tags") or contains(@class, "article_reactions")]', + '//*[contains(@class, "OUTBRAIN") or contains(@class, "related_item") or contains(@class, "share")]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php new file mode 100644 index 0000000..0747d0f --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.universfreebox.com/article/24305/4G-Bouygues-Telecom-lance-une-vente-flash-sur-son-forfait-Sensation-3Go', + 'body' => array( + '//div[@id="corps_corps"]', + ), + 'strip' => array( + '//*[@id="formulaire"]', + '//*[@id="commentaire"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php new file mode 100644 index 0000000..316c265 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php @@ -0,0 +1,41 @@ + array( + '%^/zeit-magazin.*%' => array( + 'test_url' => 'http://www.zeit.de/zeit-magazin/2015/15/pegida-kathrin-oertel-lutz-bachmann', + 'body' => array( + '//article[@class="article"]', + ), + 'strip' => array( + '//header/div/h1', + '//header/div/div[@class="article__head__subtitle"]', + '//header/div/div[@class="article__column__author"]', + '//header/div/div[@class="article__column__author"]', + '//header/div/span[@class="article__head__meta-wrap"]', + '//form', + '//style', + '//div[contains(@class, "ad-tile")]', + '//div[@class="iqd-mobile-adplace"]', + '//div[@id="iq-artikelanker"]', + '//div[@id="js-social-services"]', + '//section[@id="js-comments"]', + '//aside', + ), + ), + '%.*%' => array( + 'test_url' => 'http://www.zeit.de/politik/ausland/2015-04/thessaloniki-krise-griechenland-yannis-boutaris/', + 'body' => array( + '//div[@class="article-body"]', + ), + 'strip' => array( + '//*[@class="articleheader"]', + '//*[@class="excerpt"]', + '//div[contains(@class, "ad")]', + '//div[@itemprop="video"]', + '//*[@class="articlemeta"]', + '//*[@class="articlemeta-clear"]', + '//*[@class="zol_inarticletools"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php new file mode 100644 index 0000000..8495726 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%alt="(.+)" */>%' => '/>
$1', + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php new file mode 100644 index 0000000..9fd83f1 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'https://ymatuhin.ru/tools/git-default-editor/', + 'body' => array( + '//section', + ), + 'strip' => array( + "//script", + "//style", + "//h1", + "//time", + "//aside", + "/html/body/section/ul", + "//amp-iframe", + "/html/body/section/h4" + ), + ) + ) +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php new file mode 100644 index 0000000..36d3bdf --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'https://zarojel.hu/meg-egyszer-a-foldi-ugyrol-is/', + 'body' => array( + '//div[@class="entry-category"]/h1', + '//div[@class="entry-content"]/div[@class="vc_row wpb_row vc_row-fluid"]' + ), + 'strip' => array( + '//ins[@class="adsbygoogle"]', + '//script', + '//figcaption', + '//p[contains(text(),"Kapcsolódó")]', + '//div[@class="wpb_wrapper"]/p[@class="entry-title"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php new file mode 100644 index 0000000..79b35dd --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://zdnet.com.feedsportal.com/c/35462/f/675637/s/4a33c93e/sc/11/l/0L0Szdnet0N0Carticle0Cchina0Eus0Eagree0Eon0Ecybercrime0Ecooperation0Eamid0Econtinued0Etension0C0Tftag0FRSSbaffb68/story01.htm', + 'body' => array( + '//p[@class="summary"]', + '//div[contains(@class,"storyBody")]', + ), + 'strip' => array( + '//*[contains(@class,"ad-")]', + '//p/span', + '//script', + '//p[@class="summary"]', + '//div[contains(@class,"relatedContent")]', + '//div[contains(@class,"loader")]', + '//p[@class="photoDetails"]', + '//div[@class="thumbnailSlider"]', + '//div[@class="shortcodeGalleryWrapper"]', + ), + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php new file mode 100644 index 0000000..3e38781 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://zoom.hu/2017/10/20/mar-nem-nyomoznak-a-vegrehajtok-botranyai-miatt', + 'body' => array( + '//div[@class="title-wrapper"]/h1', + '//div[@class="entry-excerpt"]', + '//div[@class="thumbnail-wrapper"]', + '//div[@id="entry-content-id"]' + ), + 'strip' => array( + '//div[@class="place first normal"]' + ) + ), + ), +); diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php new file mode 100644 index 0000000..0f74b3d --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php @@ -0,0 +1,281 @@ +dom = XmlParser::getHtmlDocument(''.$html); + $this->xpath = new DOMXPath($this->dom); + } + + /** + * Get the relevant content with the list of potential attributes. + * + * @return string + */ + public function execute() + { + $content = $this->findContentWithCandidates(); + + if (strlen($content) < 200) { + $content = $this->findContentWithArticle(); + } + + if (strlen($content) < 50) { + $content = $this->findContentWithBody(); + } + + return $this->stripGarbage($content); + } + + /** + * Find content based on the list of tag candidates. + * + * @return string + */ + public function findContentWithCandidates() + { + foreach ($this->candidatesAttributes as $candidate) { + Logger::setMessage(get_called_class().': Try this candidate: "'.$candidate.'"'); + + $nodes = $this->xpath->query('//*[(contains(@class, "'.$candidate.'") or @id="'.$candidate.'") and not (contains(@class, "nav") or contains(@class, "page"))]'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Find candidate "'.$candidate.'"'); + + return $this->dom->saveXML($nodes->item(0)); + } + } + + return ''; + } + + /** + * Find
tag. + * + * @return string + */ + public function findContentWithArticle() + { + $nodes = $this->xpath->query('//article'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Find
tag'); + + return $this->dom->saveXML($nodes->item(0)); + } + + return ''; + } + + /** + * Find tag. + * + * @return string + */ + public function findContentWithBody() + { + $nodes = $this->xpath->query('//body'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().' Find '); + + return $this->dom->saveXML($nodes->item(0)); + } + + return ''; + } + + /** + * Strip useless tags. + * + * @param string $content + * @return string + */ + public function stripGarbage($content) + { + $dom = XmlParser::getDomDocument($content); + + if ($dom !== false) { + $xpath = new DOMXPath($dom); + + $this->stripTags($xpath); + $this->stripAttributes($dom, $xpath); + + $content = $dom->saveXML($dom->documentElement); + } + + return $content; + } + + /** + * Remove blacklisted tags. + * + * @param DOMXPath $xpath + */ + public function stripTags(DOMXPath $xpath) + { + foreach ($this->stripTags as $tag) { + $nodes = $xpath->query('//'.$tag); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Strip tag: "'.$tag.'"'); + + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + } + } + } + + /** + * Remove blacklisted attributes. + * + * @param DomDocument $dom + * @param DOMXPath $xpath + */ + public function stripAttributes(DomDocument $dom, DOMXPath $xpath) + { + foreach ($this->stripAttributes as $attribute) { + $nodes = $xpath->query('//*[contains(@class, "'.$attribute.'") or contains(@id, "'.$attribute.'")]'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Strip attribute: "'.$attribute.'"'); + + foreach ($nodes as $node) { + if ($this->shouldRemove($dom, $node)) { + $node->parentNode->removeChild($node); + } + } + } + } + } + + /** + * Find link for next page of the article. + * + * @return string + */ + public function findNextLink() + { + return null; + } + + /** + * Return false if the node should not be removed. + * + * @param DomDocument $dom + * @param \DomNode $node + * @return bool + */ + public function shouldRemove(DomDocument $dom, $node) + { + $document_length = strlen($dom->textContent); + $node_length = strlen($node->textContent); + + if ($document_length === 0) { + return true; + } + + $ratio = $node_length * 100 / $document_length; + + if ($ratio >= 90) { + Logger::setMessage(get_called_class().': Should not remove this node ('.$node->nodeName.') ratio: '.$ratio.'%'); + + return false; + } + + return true; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php new file mode 100644 index 0000000..3ded4b1 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php @@ -0,0 +1,20 @@ +getRulesFileList($hostname); + + foreach ($this->getRulesFolders() as $folder) { + $rule = $this->loadRuleFile($folder, $files); + + if (!empty($rule)) { + return $rule; + } + } + } + + return array(); + } + + /** + * Get the list of possible rules file names for a given hostname. + * + * @param string $hostname Hostname + * @return array + */ + public function getRulesFileList($hostname) + { + $files = array($hostname); // subdomain.domain.tld + $parts = explode('.', $hostname); + $len = count($parts); + + if ($len > 2) { + $subdomain = array_shift($parts); + $files[] = implode('.', $parts); // domain.tld + $files[] = '.'.implode('.', $parts); // .domain.tld + $files[] = $subdomain; // subdomain + } elseif ($len === 2) { + $files[] = '.'.implode('.', $parts); // .domain.tld + $files[] = $parts[0]; // domain + } + + return $files; + } + + /** + * Load a rule file from the defined folder. + * + * @param string $folder Rule directory + * @param array $files List of possible file names + * @return array + */ + public function loadRuleFile($folder, array $files) + { + foreach ($files as $file) { + $filename = $folder.'/'.$file.'.php'; + if (file_exists($filename)) { + Logger::setMessage(get_called_class().' Load rule: '.$file); + + return include $filename; + } + } + + return array(); + } + + /** + * Get the list of folders that contains rules. + * + * @return array + */ + public function getRulesFolders() + { + $folders = array(); + + if ($this->config !== null && $this->config->getGrabberRulesFolder() !== null) { + $folders[] = $this->config->getGrabberRulesFolder(); + } + + $folders[] = __DIR__ . '/../Rules'; + + return $folders; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php new file mode 100644 index 0000000..9beb59c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php @@ -0,0 +1,102 @@ +rules = $rules; + $this->dom = XmlParser::getHtmlDocument(''.$html); + $this->xpath = new DOMXPath($this->dom); + } + + /** + * Get the relevant content with predefined rules. + * + * @return string + */ + public function execute() + { + $this->stripTags(); + + return $this->findContent(); + } + + /** + * Remove HTML tags. + */ + public function stripTags() + { + if (isset($this->rules['strip']) && is_array($this->rules['strip'])) { + foreach ($this->rules['strip'] as $pattern) { + $nodes = $this->xpath->query($pattern); + + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + } + } + } + } + + /** + * Fetch content based on Xpath rules. + */ + public function findContent() + { + $content = ''; + if (isset($this->rules['body']) && is_array($this->rules['body'])) { + foreach ($this->rules['body'] as $pattern) { + $nodes = $this->xpath->query($pattern); + + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + $content .= $this->dom->saveXML($node); + } + } + } + } + + return $content; + } + + /** + * Fetch next link based on Xpath rules. + * + * @return string + */ + public function findNextLink() + { + if (isset($this->rules['next_page']) && is_array($this->rules['next_page'])) { + foreach ($this->rules['next_page'] as $pattern) { + $nodes = $this->xpath->query($pattern); + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + return $node->getAttribute('href'); + } + } + } + } + return null; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php new file mode 100644 index 0000000..29383b2 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php @@ -0,0 +1,282 @@ +enableCandidateParser = false; + return $this; + } + + /** + * Get encoding. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set encoding. + * + * @param string $encoding + * + * @return Scraper + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + + return $this; + } + + /** + * Get URL to download. + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL to download. + * + * @param string $url URL + * + * @return Scraper + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Return true if the scraper found relevant content. + * + * @return bool + */ + public function hasRelevantContent() + { + return !empty($this->content); + } + + /** + * Get relevant content. + * + * @return string + */ + public function getRelevantContent() + { + return $this->content; + } + + /** + * Get raw content (unfiltered). + * + * @return string + */ + public function getRawContent() + { + return $this->html; + } + + /** + * Set raw content (unfiltered). + * + * @param string $html + * + * @return Scraper + */ + public function setRawContent($html) + { + $this->html = $html; + + return $this; + } + + /** + * Get filtered relevant content. + * + * @return string + */ + public function getFilteredContent() + { + $filter = Filter::html($this->content, $this->url); + $filter->setConfig($this->config); + + return $filter->execute(); + } + + /** + * Download the HTML content. + * + * @return bool + */ + public function download() + { + if (!empty($this->url)) { + + // Clear everything + $this->html = ''; + $this->content = ''; + $this->encoding = ''; + + try { + $client = Client::getInstance(); + $client->setConfig($this->config); + $client->setTimeout($this->config->getGrabberTimeout()); + $client->setUserAgent($this->config->getGrabberUserAgent()); + $client->execute($this->url); + + $this->url = $client->getUrl(); + $this->html = $client->getContent(); + $this->encoding = $client->getEncoding(); + + return true; + } catch (ClientException $e) { + Logger::setMessage(get_called_class().': '.$e->getMessage()); + } + } + + return false; + } + + /** + * Execute the scraper. + * + * @param string $pageContent + * @param int $recursionDepth + */ + public function execute($pageContent = '', $recursionDepth = 0) + { + $this->html = ''; + $this->encoding = ''; + $this->content = ''; + $this->download(); + $this->prepareHtml(); + + $parser = $this->getParser(); + + if ($parser !== null) { + $maxRecursions = $this->config->getMaxRecursions(); + if(!isset($maxRecursions)){ + $maxRecursions = 25; + } + $pageContent .= $parser->execute(); + // check if there is a link to next page and recursively get content (max 25 pages) + if((($nextLink = $parser->findNextLink()) !== null) && $recursionDepth < $maxRecursions){ + $nextLink = Url::resolve($nextLink,$this->url); + $this->setUrl($nextLink); + $this->execute($pageContent,$recursionDepth+1); + } + else{ + $this->content = $pageContent; + } + Logger::setMessage(get_called_class().': Content length: '.strlen($this->content).' bytes'); + } + } + + /** + * Get the parser. + * + * @return ParserInterface + */ + public function getParser() + { + $ruleLoader = new RuleLoader($this->config); + $rules = $ruleLoader->getRules($this->url); + + if (!empty($rules['grabber'])) { + Logger::setMessage(get_called_class().': Parse content with rules'); + + foreach ($rules['grabber'] as $pattern => $rule) { + $url = new Url($this->url); + $sub_url = $url->getFullPath(); + + if (preg_match($pattern, $sub_url)) { + Logger::setMessage(get_called_class().': Matched url '.$sub_url); + return new RuleParser($this->html, $rule); + } + } + } elseif ($this->enableCandidateParser) { + Logger::setMessage(get_called_class().': Parse content with candidates'); + } + + return new CandidateParser($this->html); + } + + /** + * Normalize encoding and strip head tag. + */ + public function prepareHtml() + { + $html_encoding = XmlParser::getEncodingFromMetaTag($this->html); + + $this->html = Encoding::convert($this->html, $html_encoding ?: $this->encoding); + $this->html = Filter::stripHeadTags($this->html); + + Logger::setMessage(get_called_class().': HTTP Encoding "'.$this->encoding.'" ; HTML Encoding "'.$html_encoding.'"'); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php new file mode 100644 index 0000000..12eccfd --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php @@ -0,0 +1,175 @@ +title = $title; + return $this; + } + + /** + * Get title + * + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set feed URL + * + * @access public + * @param string $feedUrl + * @return Subscription + */ + public function setFeedUrl($feedUrl) + { + $this->feedUrl = $feedUrl; + return $this; + } + + /** + * Get feed URL + * + * @access public + * @return string + */ + public function getFeedUrl() + { + return $this->feedUrl; + } + + /** + * Set site URL + * + * @access public + * @param string $siteUrl + * @return Subscription + */ + public function setSiteUrl($siteUrl) + { + $this->siteUrl = $siteUrl; + return $this; + } + + /** + * Get site URL + * + * @access public + * @return string + */ + public function getSiteUrl() + { + return $this->siteUrl; + } + + /** + * Set category + * + * @access public + * @param string $category + * @return Subscription + */ + public function setCategory($category) + { + $this->category = $category; + return $this; + } + + /** + * Get category + * + * @access public + * @return string + */ + public function getCategory() + { + return $this->category; + } + + /** + * Set description + * + * @access public + * @param string $description + * @return Subscription + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Get description + * + * @access public + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set type + * + * @access public + * @param string $type + * @return Subscription + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Get type + * + * @access public + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php new file mode 100644 index 0000000..b173f89 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php @@ -0,0 +1,75 @@ +title = $title; + return $this; + } + + /** + * Get title + * + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Add subscription + * + * @access public + * @param Subscription $subscription + * @return SubscriptionList + */ + public function addSubscription(Subscription $subscription) + { + $this->subscriptions[] = $subscription; + return $this; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php new file mode 100644 index 0000000..838e4cb --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php @@ -0,0 +1,204 @@ +subscriptionList = $subscriptionList; + } + + /** + * Get object instance + * + * @static + * @access public + * @param SubscriptionList $subscriptionList + * @return SubscriptionListBuilder + */ + public static function create(SubscriptionList $subscriptionList) + { + return new static($subscriptionList); + } + + /** + * Build OPML feed + * + * @access public + * @param string $filename + * @return string + */ + public function build($filename = '') + { + $this->document = new DomDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + + $opmlElement = $this->document->createElement('opml'); + $opmlElement->setAttribute('version', '1.0'); + + $headElement = $this->document->createElement('head'); + + if ($this->subscriptionList->getTitle() !== '') { + $titleElement = $this->document->createElement('title'); + $titleElement->appendChild($this->document->createTextNode($this->subscriptionList->getTitle())); + $headElement->appendChild($titleElement); + } + + $opmlElement->appendChild($headElement); + $opmlElement->appendChild($this->buildBody()); + $this->document->appendChild($opmlElement); + + if ($filename !== '') { + $this->document->save($filename); + return ''; + } + + return $this->document->saveXML(); + } + + /** + * Return true if the list has categories + * + * @access public + * @return bool + */ + public function hasCategories() + { + foreach ($this->subscriptionList->subscriptions as $subscription) { + if ($subscription->getCategory() !== '') { + return true; + } + } + + return false; + } + + /** + * Build OPML body + * + * @access protected + * @return DOMElement + */ + protected function buildBody() + { + $bodyElement = $this->document->createElement('body'); + + if ($this->hasCategories()) { + $this->buildCategories($bodyElement); + return $bodyElement; + } + + foreach ($this->subscriptionList->subscriptions as $subscription) { + $bodyElement->appendChild($this->buildSubscription($subscription)); + } + + return $bodyElement; + } + + /** + * Build categories section + * + * @access protected + * @param DOMElement $bodyElement + */ + protected function buildCategories(DOMElement $bodyElement) + { + $categories = $this->groupByCategories(); + + foreach ($categories as $category => $subscriptions) { + $bodyElement->appendChild($this->buildCategory($category, $subscriptions)); + } + } + + /** + * Build category tag + * + * @access protected + * @param string $category + * @param array $subscriptions + * @return DOMElement + */ + protected function buildCategory($category, array $subscriptions) + { + $outlineElement = $this->document->createElement('outline'); + $outlineElement->setAttribute('text', $category); + + foreach ($subscriptions as $subscription) { + $outlineElement->appendChild($this->buildSubscription($subscription)); + } + + return $outlineElement; + } + + /** + * Build subscription entry + * + * @access public + * @param Subscription $subscription + * @return DOMElement + */ + protected function buildSubscription(Subscription $subscription) + { + $outlineElement = $this->document->createElement('outline'); + $outlineElement->setAttribute('type', $subscription->getType() ?: 'rss'); + $outlineElement->setAttribute('text', $subscription->getTitle() ?: $subscription->getFeedUrl()); + $outlineElement->setAttribute('xmlUrl', $subscription->getFeedUrl()); + + if ($subscription->getTitle() !== '') { + $outlineElement->setAttribute('title', $subscription->getTitle()); + } + + if ($subscription->getDescription() !== '') { + $outlineElement->setAttribute('description', $subscription->getDescription()); + } + + if ($subscription->getSiteUrl() !== '') { + $outlineElement->setAttribute('htmlUrl', $subscription->getSiteUrl()); + } + + return $outlineElement; + } + + /** + * Group subscriptions by category + * + * @access private + * @return array + */ + private function groupByCategories() + { + $categories = array(); + + foreach ($this->subscriptionList->subscriptions as $subscription) { + $categories[$subscription->getCategory()][] = $subscription; + } + + return $categories; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php new file mode 100644 index 0000000..9085588 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php @@ -0,0 +1,100 @@ +subscriptionList = new SubscriptionList(); + $this->data = trim($data); + } + + /** + * Get object instance + * + * @static + * @access public + * @param string $data + * @return SubscriptionListParser + */ + public static function create($data) + { + return new static($data); + } + + /** + * Parse a subscription list entry + * + * @access public + * @throws MalformedXmlException + * @return SubscriptionList + */ + public function parse() + { + $xml = XmlParser::getSimpleXml($this->data); + + if (! $xml || !isset($xml->head) || !isset($xml->body)) { + throw new MalformedXmlException('Unable to parse OPML file: invalid XML'); + } + + $this->parseTitle($xml->head); + $this->parseEntries($xml->body); + + return $this->subscriptionList; + } + + /** + * Parse title + * + * @access protected + * @param SimpleXMLElement $xml + */ + protected function parseTitle(SimpleXMLElement $xml) + { + $this->subscriptionList->setTitle((string) $xml->title); + } + + /** + * Parse entries + * + * @access protected + * @param SimpleXMLElement $body + */ + private function parseEntries(SimpleXMLElement $body) + { + foreach ($body->outline as $outlineElement) { + if (isset($outlineElement->outline)) { + $this->parseEntries($outlineElement); + } else { + $this->subscriptionList->subscriptions[] = SubscriptionParser::create($body, $outlineElement)->parse(); + } + } + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php new file mode 100644 index 0000000..caff07c --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php @@ -0,0 +1,142 @@ +parentElement = $parentElement; + $this->outlineElement = $outlineElement; + $this->subscription = new Subscription(); + } + + /** + * Get object instance + * + * @static + * @access public + * @param SimpleXMLElement $parentElement + * @param SimpleXMLElement $outlineElement + * @return SubscriptionParser + */ + public static function create(SimpleXMLElement $parentElement, SimpleXMLElement $outlineElement) + { + return new static($parentElement, $outlineElement); + } + + /** + * Parse subscription entry + * + * @access public + * @return Subscription + */ + public function parse() + { + $this->subscription->setCategory($this->findCategory()); + $this->subscription->setTitle($this->findTitle()); + $this->subscription->setFeedUrl($this->findFeedUrl()); + $this->subscription->setSiteUrl($this->findSiteUrl()); + $this->subscription->setType($this->findType()); + $this->subscription->setDescription($this->findDescription()); + + return $this->subscription; + } + + /** + * Find category. + * + * @access protected + * @return string + */ + protected function findCategory() + { + return isset($this->parentElement['text']) ? (string) $this->parentElement['text'] : ''; + } + + /** + * Find title. + * + * @access protected + * @return string + */ + protected function findTitle() + { + return isset($this->outlineElement['title']) ? (string) $this->outlineElement['title'] : (string) $this->outlineElement['text']; + } + + /** + * Find feed url. + * + * @access protected + * @return string + */ + protected function findFeedUrl() + { + return (string) $this->outlineElement['xmlUrl']; + } + + /** + * Find site url. + * + * @access protected + * @return string + */ + protected function findSiteUrl() + { + return isset($this->outlineElement['htmlUrl']) ? (string) $this->outlineElement['htmlUrl'] : $this->findFeedUrl(); + } + + /** + * Find type. + * + * @access protected + * @return string + */ + protected function findType() + { + return isset($this->outlineElement['version']) ? (string) $this->outlineElement['version'] : + isset($this->outlineElement['type']) ? (string) $this->outlineElement['type'] : 'rss'; + } + + /** + * Find description. + * + * @access protected + * @return string + */ + protected function findDescription() + { + return isset($this->outlineElement['description']) ? (string) $this->outlineElement['description'] : $this->findTitle(); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php new file mode 100644 index 0000000..34f3780 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php @@ -0,0 +1,65 @@ +helper = new AtomHelper($this->getDocument()); + + $this->feedElement = $this->getDocument()->createElement('feed'); + $this->feedElement->setAttributeNodeNS(new DomAttr('xmlns', 'http://www.w3.org/2005/Atom')); + + $generator = $this->getDocument()->createElement('generator', 'PicoFeed'); + $generator->setAttribute('uri', 'https://github.com/miniflux/picoFeed'); + $this->feedElement->appendChild($generator); + + $this->helper + ->buildTitle($this->feedElement, $this->feedTitle) + ->buildId($this->feedElement, $this->feedUrl) + ->buildDate($this->feedElement, $this->feedDate) + ->buildLink($this->feedElement, $this->siteUrl) + ->buildLink($this->feedElement, $this->feedUrl, 'self', 'application/atom+xml') + ->buildAuthor($this->feedElement, $this->authorName, $this->authorEmail, $this->authorUrl) + ; + + foreach ($this->items as $item) { + $this->feedElement->appendChild($item->build()); + } + + $this->getDocument()->appendChild($this->feedElement); + + if ($filename !== '') { + $this->getDocument()->save($filename); + } + + return $this->getDocument()->saveXML(); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php new file mode 100644 index 0000000..def6b0b --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php @@ -0,0 +1,139 @@ +document = $document; + } + + /** + * Build node + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $value + * @return AtomHelper + */ + public function buildNode(DOMElement $element, $tag, $value) + { + $node = $this->document->createElement($tag); + $node->appendChild($this->document->createTextNode($value)); + $element->appendChild($node); + return $this; + } + + /** + * Build title + * + * @access public + * @param DOMElement $element + * @param string $title + * @return AtomHelper + */ + public function buildTitle(DOMElement $element, $title) + { + return $this->buildNode($element, 'title', $title); + } + + /** + * Build id + * + * @access public + * @param DOMElement $element + * @param string $id + * @return AtomHelper + */ + public function buildId(DOMElement $element, $id) + { + return $this->buildNode($element, 'id', $id); + } + + /** + * Build date element + * + * @access public + * @param DOMElement $element + * @param DateTime $date + * @param string $type + * @return AtomHelper + */ + public function buildDate(DOMElement $element, DateTime $date, $type = 'updated') + { + return $this->buildNode($element, $type, $date->format(DateTime::ATOM)); + } + + /** + * Build link element + * + * @access public + * @param DOMElement $element + * @param string $url + * @param string $rel + * @param string $type + * @return AtomHelper + */ + public function buildLink(DOMElement $element, $url, $rel = 'alternate', $type = 'text/html') + { + $node = $this->document->createElement('link'); + $node->setAttribute('rel', $rel); + $node->setAttribute('type', $type); + $node->setAttribute('href', $url); + $element->appendChild($node); + + return $this; + } + + /** + * Build author element + * + * @access public + * @param DOMElement $element + * @param string $authorName + * @param string $authorEmail + * @param string $authorUrl + * @return AtomHelper + */ + public function buildAuthor(DOMElement $element, $authorName, $authorEmail, $authorUrl) + { + if (!empty($authorName)) { + $author = $this->document->createElement('author'); + $this->buildNode($author, 'name', $authorName); + + if (!empty($authorEmail)) { + $this->buildNode($author, 'email', $authorEmail); + } + + if (!empty($authorUrl)) { + $this->buildNode($author, 'uri', $authorUrl); + } + + $element->appendChild($author); + } + + return $this; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php new file mode 100644 index 0000000..dfdfe68 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php @@ -0,0 +1,63 @@ +itemElement = $this->feedBuilder->getDocument()->createElement('entry'); + $this->helper = new AtomHelper($this->feedBuilder->getDocument()); + + if (!empty($this->itemId)) { + $this->helper->buildId($this->itemElement, $this->itemId); + } else { + $this->helper->buildId($this->itemElement, $this->itemUrl); + } + + $this->helper + ->buildTitle($this->itemElement, $this->itemTitle) + ->buildLink($this->itemElement, $this->itemUrl) + ->buildDate($this->itemElement, $this->itemUpdatedDate, 'updated') + ->buildDate($this->itemElement, $this->itemPublishedDate, 'published') + ->buildAuthor($this->itemElement, $this->authorName, $this->authorEmail, $this->authorUrl) + ; + + if (!empty($this->itemSummary)) { + $this->helper->buildNode($this->itemElement, 'summary', $this->itemSummary); + } + + if (!empty($this->itemContent)) { + $node = $this->feedBuilder->getDocument()->createElement('content'); + $node->setAttribute('type', 'html'); + $node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent)); + $this->itemElement->appendChild($node); + } + + return $this->itemElement; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php new file mode 100644 index 0000000..cf9d024 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php @@ -0,0 +1,185 @@ +document = new DomDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + } + + /** + * Get new object instance + * + * @access public + * @return static + */ + public static function create() + { + return new static(); + } + + /** + * Add feed title + * + * @access public + * @param string $title + * @return $this + */ + public function withTitle($title) + { + $this->feedTitle = $title; + return $this; + } + + /** + * Add feed url + * + * @access public + * @param string $url + * @return $this + */ + public function withFeedUrl($url) + { + $this->feedUrl = $url; + return $this; + } + + /** + * Add website url + * + * @access public + * @param string $url + * @return $this + */ + public function withSiteUrl($url) + { + $this->siteUrl = $url; + return $this; + } + + /** + * Add feed date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withDate(DateTime $date) + { + $this->feedDate = $date; + return $this; + } + + /** + * Add feed author + * + * @access public + * @param string $name + * @param string $email + * @param string $url + * @return $this + */ + public function withAuthor($name, $email = '', $url ='') + { + $this->authorName = $name; + $this->authorEmail = $email; + $this->authorUrl = $url; + return $this; + } + + /** + * Add feed item + * + * @access public + * @param ItemBuilder $item + * @return $this + */ + public function withItem(ItemBuilder $item) + { + $this->items[] = $item; + return $this; + } + + /** + * Get DOM document + * + * @access public + * @return DOMDocument + */ + public function getDocument() + { + return $this->document; + } + + /** + * Build feed + * + * @abstract + * @access public + * @param string $filename + * @return string + */ + abstract public function build($filename = ''); +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php new file mode 100644 index 0000000..86985bc --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php @@ -0,0 +1,209 @@ +feedBuilder = $feedBuilder; + } + + /** + * Get new object instance + * + * @access public + * @param FeedBuilder $feedBuilder + * @return static + */ + public static function create(FeedBuilder $feedBuilder) + { + return new static($feedBuilder); + } + + /** + * Add item title + * + * @access public + * @param string $title + * @return $this + */ + public function withTitle($title) + { + $this->itemTitle = $title; + return $this; + } + + /** + * Add item id + * + * @access public + * @param string $id + * @return $this + */ + public function withId($id) + { + $this->itemId = $id; + return $this; + } + + /** + * Add item url + * + * @access public + * @param string $url + * @return $this + */ + public function withUrl($url) + { + $this->itemUrl = $url; + return $this; + } + + /** + * Add item summary + * + * @access public + * @param string $summary + * @return $this + */ + public function withSummary($summary) + { + $this->itemSummary = $summary; + return $this; + } + + /** + * Add item content + * + * @access public + * @param string $content + * @return $this + */ + public function withContent($content) + { + $this->itemContent = $content; + return $this; + } + + /** + * Add item updated date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withUpdatedDate(DateTime $date) + { + $this->itemUpdatedDate = $date; + return $this; + } + + /** + * Add item published date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withPublishedDate(DateTime $date) + { + $this->itemPublishedDate = $date; + return $this; + } + + /** + * Add item author + * + * @access public + * @param string $name + * @param string $email + * @param string $url + * @return $this + */ + public function withAuthor($name, $email = '', $url ='') + { + $this->authorName = $name; + $this->authorEmail = $email; + $this->authorUrl = $url; + return $this; + } + + /** + * Build item + * + * @abstract + * @access public + * @return DOMElement + */ + abstract public function build(); +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php new file mode 100644 index 0000000..bc3f513 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php @@ -0,0 +1,76 @@ +helper = new Rss20Helper($this->getDocument()); + + $this->rssElement = $this->getDocument()->createElement('rss'); + $this->rssElement->setAttribute('version', '2.0'); + $this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:content', 'http://purl.org/rss/1.0/modules/content/')); + $this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:atom', 'http://www.w3.org/2005/Atom')); + + $this->channelElement = $this->getDocument()->createElement('channel'); + $this->helper + ->buildNode($this->channelElement, 'generator', 'PicoFeed (https://github.com/miniflux/picoFeed)') + ->buildTitle($this->channelElement, $this->feedTitle) + ->buildNode($this->channelElement, 'description', $this->feedTitle) + ->buildDate($this->channelElement, $this->feedDate) + ->buildAuthor($this->channelElement, 'webMaster', $this->authorName, $this->authorEmail) + ->buildLink($this->channelElement, $this->siteUrl) + ; + + $link = $this->getDocument()->createElement('atom:link'); + $link->setAttribute('href', $this->feedUrl); + $link->setAttribute('rel', 'self'); + $link->setAttribute('type', 'application/rss+xml'); + $this->channelElement->appendChild($link); + + foreach ($this->items as $item) { + $this->channelElement->appendChild($item->build()); + } + + $this->rssElement->appendChild($this->channelElement); + $this->getDocument()->appendChild($this->rssElement); + + if ($filename !== '') { + $this->getDocument()->save($filename); + } + + return $this->getDocument()->saveXML(); + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php new file mode 100644 index 0000000..72a19e5 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php @@ -0,0 +1,115 @@ +document = $document; + } + + /** + * Build node + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $value + * @return $this + */ + public function buildNode(DOMElement $element, $tag, $value) + { + $node = $this->document->createElement($tag); + $node->appendChild($this->document->createTextNode($value)); + $element->appendChild($node); + return $this; + } + + /** + * Build title + * + * @access public + * @param DOMElement $element + * @param string $title + * @return $this + */ + public function buildTitle(DOMElement $element, $title) + { + return $this->buildNode($element, 'title', $title); + } + + /** + * Build date element + * + * @access public + * @param DOMElement $element + * @param DateTime $date + * @param string $type + * @return $this + */ + public function buildDate(DOMElement $element, DateTime $date, $type = 'pubDate') + { + return $this->buildNode($element, $type, $date->format(DateTime::RSS)); + } + + /** + * Build link element + * + * @access public + * @param DOMElement $element + * @param string $url + * @return $this + */ + public function buildLink(DOMElement $element, $url) + { + return $this->buildNode($element, 'link', $url); + } + + /** + * Build author element + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $authorName + * @param string $authorEmail + * @return $this + */ + public function buildAuthor(DOMElement $element, $tag, $authorName, $authorEmail) + { + if (!empty($authorName)) { + $value = ''; + + if (!empty($authorEmail)) { + $value .= $authorEmail.' ('.$authorName.')'; + } else { + $value = $authorName; + } + + $this->buildNode($element, $tag, $value); + } + + return $this; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php new file mode 100644 index 0000000..125dc6a --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php @@ -0,0 +1,67 @@ +itemElement = $this->feedBuilder->getDocument()->createElement('item'); + $this->helper = new Rss20Helper($this->feedBuilder->getDocument()); + + if (!empty($this->itemId)) { + $guid = $this->feedBuilder->getDocument()->createElement('guid'); + $guid->setAttribute('isPermaLink', 'false'); + $guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemId)); + $this->itemElement->appendChild($guid); + } else { + $guid = $this->feedBuilder->getDocument()->createElement('guid'); + $guid->setAttribute('isPermaLink', 'true'); + $guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemUrl)); + $this->itemElement->appendChild($guid); + } + + $this->helper + ->buildTitle($this->itemElement, $this->itemTitle) + ->buildLink($this->itemElement, $this->itemUrl) + ->buildDate($this->itemElement, $this->itemPublishedDate) + ->buildAuthor($this->itemElement, 'author', $this->authorName, $this->authorEmail) + ; + + if (!empty($this->itemSummary)) { + $this->helper->buildNode($this->itemElement, 'description', $this->itemSummary); + } + + if (!empty($this->itemContent)) { + $node = $this->feedBuilder->getDocument()->createElement('content:encoded'); + $node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent)); + $this->itemElement->appendChild($node); + } + + return $this->itemElement; + } +} diff --git a/plugins/admin/vendor/p3k/picofeed/picofeed b/plugins/admin/vendor/p3k/picofeed/picofeed new file mode 100644 index 0000000..8f35737 --- /dev/null +++ b/plugins/admin/vendor/p3k/picofeed/picofeed @@ -0,0 +1,135 @@ +#!/usr/bin/env php +discover($url); + + $parser = $reader->getParser( + $resource->getUrl(), + $resource->getContent(), + $resource->getEncoding() + ); + + if ($disable_filtering) { + $parser->disableContentFiltering(); + } + + return $parser->execute(); + } + catch (PicoFeedException $e) { + echo 'Exception thrown ===> "'.$e->getMessage().'"'.PHP_EOL; + return false; + } +} + +function get_item($feed, $item_id) +{ + foreach ($feed->items as $item) { + if ($item->getId() === $item_id) { + echo $item; + echo "============= CONTENT ================\n"; + echo $item->getContent(); + echo "\n============= CONTENT ================\n"; + break; + } + } +} + +function dump_feed($url) +{ + $feed = get_feed($url); + echo $feed; +} + +function debug_feed($url) +{ + get_feed($url); + print_r(Logger::getMessages()); +} + +function dump_item($url, $item_id) +{ + $feed = get_feed($url); + + if ($feed !== false) { + get_item($feed, $item_id); + } +} + +function nofilter_item($url, $item_id) +{ + $feed = get_feed($url, true); + + if ($feed !== false) { + get_item($feed, $item_id); + } +} + +function grabber($url) +{ + $grabber = new Scraper(new Config); + $grabber->setUrl($url); + $grabber->execute(); + + print_r(Logger::getMessages()); + echo "============= CONTENT ================\n"; + echo $grabber->getRelevantContent().PHP_EOL; + echo "============= FILTERED ================\n"; + echo $grabber->getFilteredContent().PHP_EOL; +} + +function fetch_favicon($url) +{ + $favicon = new Favicon(); + echo $favicon->find($url) . PHP_EOL; +} + +// Parse command line arguments +if ($argc === 4) { + switch ($argv[1]) { + case 'item': + dump_item($argv[2], $argv[3]); + die; + case 'nofilter': + nofilter_item($argv[2], $argv[3]); + die; + } +} else if ($argc === 3) { + switch ($argv[1]) { + case 'feed': + dump_feed($argv[2]); + die; + case 'debug': + debug_feed($argv[2]); + die; + case 'grabber': + grabber($argv[2]); + die; + case 'favicon': + fetch_favicon($argv[2]); + die; + } +} + +printf("Usage:\n"); +printf("%s feed \n", $argv[0]); +printf("%s debug \n", $argv[0]); +printf("%s item \n", $argv[0]); +printf("%s nofilter \n", $argv[0]); +printf("%s grabber \n", $argv[0]); +printf("%s favicon \n", $argv[0]); diff --git a/plugins/admin/vendor/scssphp/scssphp/LICENSE.md b/plugins/admin/vendor/scssphp/scssphp/LICENSE.md new file mode 100644 index 0000000..afcfdfb --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2015 Leaf Corcoran, http://scssphp.github.io/scssphp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/admin/vendor/scssphp/scssphp/README.md b/plugins/admin/vendor/scssphp/scssphp/README.md new file mode 100644 index 0000000..65bb93e --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/README.md @@ -0,0 +1,71 @@ +# scssphp +### + +![Build](https://github.com/scssphp/scssphp/workflows/CI/badge.svg) +[![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp) + +`scssphp` is a compiler for SCSS written in PHP. + +Checkout the homepage, , for directions on how to use. + +## Running Tests + +`scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing. + +Run the following command from the root directory to run every test: + + vendor/bin/phpunit tests + +There are several tests in the `tests/` directory: + +* `ApiTest.php` contains various unit tests that test the PHP interface. +* `ExceptionTest.php` contains unit tests that test for exceptions thrown by the parser and compiler. +* `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs. +* `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory + then compares to the respective `.css` file in the `tests/outputs` directory. +* `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository. + +When changing any of the tests in `tests/inputs`, the tests will most likely +fail because the output has changed. Once you verify that the output is correct +you can run the following command to rebuild all the tests: + + BUILD=1 vendor/bin/phpunit tests + +This will compile all the tests, and save results into `tests/outputs`. It also +updates the list of excluded specs from sass-spec. + +To enable the full `sass-spec` compatibility tests: + + TEST_SASS_SPEC=1 vendor/bin/phpunit tests + +## Coding Standard + +`scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/). + +Run the following command from the root directory to check the code for "sniffs". + + vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php + +## Static Analysis + +`scssphp` uses [phpstan](https://phpstan.org/) for static analysis. + +Run the following command from the root directory to analyse the codebase: + + make phpstan + +As most of the codebase is composed of legacy code which cannot be type-checked +fully, the setup contains a baseline file with all errors we want to ignore. In +particular, we ignore all errors related to not specifying the types inside arrays +when these arrays correspond to the representation of Sass values and Sass AST nodes +in the parser and compiler. +When contributing, the proper process to deal with static analysis is the following: + +1. Make your change in the codebase +2. Run `make phpstan` +3. Fix errors reported by phpstan when possible +4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3 +5. Run `make phpstan-baseline` to regenerate the phpstan baseline + +Additions to the baseline will be reviewed to avoid ignoring errors that should have +been fixed. diff --git a/plugins/admin/vendor/scssphp/scssphp/bin/pscss b/plugins/admin/vendor/scssphp/scssphp/bin/pscss new file mode 100644 index 0000000..0f009d6 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/bin/pscss @@ -0,0 +1,244 @@ +#!/usr/bin/env php +parse($data)), true)); + + fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.'); + + exit(); +} + +$scss = new Compiler(); + +if ($loadPaths) { + $scss->setImportPaths($loadPaths); +} + +if ($style) { + if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) { + $scss->setOutputStyle($style); + } else { + fwrite(STDERR, "WARNING: the $style style is deprecated.\n"); + $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style)); + } +} + +$outputFile = isset($arguments[1]) ? $arguments[1] : null; +$sourceMapFile = null; + +if ($sourceMap) { + $sourceMapOptions = array( + 'outputSourceFiles' => $embedSources, + ); + if ($embedSourceMap || $outputFile === null) { + $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); + } else { + $sourceMapFile = $outputFile . '.map'; + $sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile; + $sourceMapOptions['sourceMapURL'] = basename($sourceMapFile); + $sourceMapOptions['sourceMapBasepath'] = getcwd(); + $sourceMapOptions['sourceMapFilename'] = basename($outputFile); + + $scss->setSourceMap(Compiler::SOURCE_MAP_FILE); + } + + $scss->setSourceMapOptions($sourceMapOptions); +} + +if ($encoding) { + $scss->setEncoding($encoding); +} + +try { + $result = $scss->compileString($data, $inputFile); +} catch (SassException $e) { + fwrite(STDERR, 'Error: '.$e->getMessage()."\n"); + exit(1); +} + +if ($outputFile) { + file_put_contents($outputFile, $result->getCss()); + + if ($sourceMapFile !== null && $result->getSourceMap() !== null) { + file_put_contents($sourceMapFile, $result->getSourceMap()); + } +} else { + echo $result->getCss(); +} diff --git a/plugins/admin/vendor/scssphp/scssphp/composer.json b/plugins/admin/vendor/scssphp/scssphp/composer.json new file mode 100644 index 0000000..f81203d --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/composer.json @@ -0,0 +1,117 @@ +{ + "name": "scssphp/scssphp", + "type": "library", + "description": "scssphp is a compiler for SCSS written in PHP.", + "keywords": ["css", "stylesheet", "scss", "sass", "less"], + "homepage": "http://scssphp.github.io/scssphp/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "homepage": "https://github.com/robocoder" + }, + { + "name": "Cédric Morin", + "email": "cedric@yterium.com", + "homepage": "https://github.com/Cerdic" + } + ], + "autoload": { + "psr-4": { "ScssPhp\\ScssPhp\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" } + }, + "require": { + "php": ">=5.6.0", + "ext-json": "*", + "ext-ctype": "*" + }, + "suggest": { + "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv", + "ext-iconv": "Can be used as fallback when ext-mbstring is not available" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", + "sass/sass-spec": "*", + "squizlabs/php_codesniffer": "~3.5", + "symfony/phpunit-bridge": "^5.1", + "thoughtbot/bourbon": "^7.0", + "twbs/bootstrap": "~5.0", + "twbs/bootstrap4": "4.6.1", + "zurb/foundation": "~6.5" + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "sass/sass-spec", + "version": "2022.08.19", + "source": { + "type": "git", + "url": "https://github.com/sass/sass-spec.git", + "reference": "2bdc199723a3445d5badac3ac774105698f08861" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sass/sass-spec/zipball/2bdc199723a3445d5badac3ac774105698f08861", + "reference": "2bdc199723a3445d5badac3ac774105698f08861", + "shasum": "" + } + } + }, + { + "type": "package", + "package": { + "name": "thoughtbot/bourbon", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/thoughtbot/bourbon.git", + "reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thoughtbot/bourbon/zipball/fbe338ee6807e7f7aa996d82c8a16f248bb149b3", + "reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3", + "shasum": "" + } + } + }, + { + "type": "package", + "package": { + "name": "twbs/bootstrap4", + "version": "v4.6.1", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "043a03c95a2ad6738f85b65e53b9dbdfb03b8d10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/043a03c95a2ad6738f85b65e53b9dbdfb03b8d10", + "reference": "043a03c95a2ad6738f85b65e53b9dbdfb03b8d10", + "shasum": "" + } + } + } + ], + "bin": ["bin/pscss"], + "config": { + "sort-packages": true, + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "forward-command": false, + "bin-links": false + } + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/scss.inc.php b/plugins/admin/vendor/scssphp/scssphp/scss.inc.php new file mode 100644 index 0000000..4598378 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/scss.inc.php @@ -0,0 +1,21 @@ + + * + * @internal + */ +class Range +{ + /** + * @var float|int + */ + public $first; + + /** + * @var float|int + */ + public $last; + + /** + * Initialize range + * + * @param int|float $first + * @param int|float $last + */ + public function __construct($first, $last) + { + $this->first = $first; + $this->last = $last; + } + + /** + * Test for inclusion in range + * + * @param int|float $value + * + * @return bool + */ + public function includes($value) + { + return $value >= $this->first && $value <= $this->last; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block.php b/plugins/admin/vendor/scssphp/scssphp/src/Block.php new file mode 100644 index 0000000..96668dc --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block.php @@ -0,0 +1,73 @@ + + * + * @internal + */ +class Block +{ + /** + * @var string|null + */ + public $type; + + /** + * @var Block|null + */ + public $parent; + + /** + * @var string + */ + public $sourceName; + + /** + * @var int + */ + public $sourceIndex; + + /** + * @var int + */ + public $sourceLine; + + /** + * @var int + */ + public $sourceColumn; + + /** + * @var array|null + */ + public $selectors; + + /** + * @var array + */ + public $comments; + + /** + * @var array + */ + public $children; + + /** + * @var Block|null + */ + public $selfParent; +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/AtRootBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/AtRootBlock.php new file mode 100644 index 0000000..41842c2 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/AtRootBlock.php @@ -0,0 +1,37 @@ +type = Type::T_AT_ROOT; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/CallableBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/CallableBlock.php new file mode 100644 index 0000000..a18a87c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/CallableBlock.php @@ -0,0 +1,45 @@ +type = $type; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/ContentBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/ContentBlock.php new file mode 100644 index 0000000..8708498 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/ContentBlock.php @@ -0,0 +1,38 @@ +type = Type::T_INCLUDE; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/DirectiveBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/DirectiveBlock.php new file mode 100644 index 0000000..b1d3d1a --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/DirectiveBlock.php @@ -0,0 +1,37 @@ +type = Type::T_DIRECTIVE; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/EachBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/EachBlock.php new file mode 100644 index 0000000..b328957 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/EachBlock.php @@ -0,0 +1,37 @@ +type = Type::T_EACH; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/ElseBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/ElseBlock.php new file mode 100644 index 0000000..6abb4d7 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/ElseBlock.php @@ -0,0 +1,27 @@ +type = Type::T_ELSE; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/ElseifBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/ElseifBlock.php new file mode 100644 index 0000000..4622bca --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/ElseifBlock.php @@ -0,0 +1,32 @@ +type = Type::T_ELSEIF; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/ForBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/ForBlock.php new file mode 100644 index 0000000..a9cf673 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/ForBlock.php @@ -0,0 +1,47 @@ +type = Type::T_FOR; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/IfBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/IfBlock.php new file mode 100644 index 0000000..9f21bf8 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/IfBlock.php @@ -0,0 +1,37 @@ + + */ + public $cases = []; + + public function __construct() + { + $this->type = Type::T_IF; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/MediaBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/MediaBlock.php new file mode 100644 index 0000000..c49ee1b --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/MediaBlock.php @@ -0,0 +1,37 @@ +type = Type::T_MEDIA; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php new file mode 100644 index 0000000..1ea4a6c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php @@ -0,0 +1,37 @@ +type = Type::T_NESTED_PROPERTY; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Block/WhileBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Block/WhileBlock.php new file mode 100644 index 0000000..ac18d4e --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Block/WhileBlock.php @@ -0,0 +1,32 @@ +type = Type::T_WHILE; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Cache.php b/plugins/admin/vendor/scssphp/scssphp/src/Cache.php new file mode 100644 index 0000000..9731c60 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Cache.php @@ -0,0 +1,272 @@ + + * + * @internal + */ +class Cache +{ + const CACHE_VERSION = 1; + + /** + * directory used for storing data + * + * @var string|false + */ + public static $cacheDir = false; + + /** + * prefix for the storing data + * + * @var string + */ + public static $prefix = 'scssphp_'; + + /** + * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit + * + * @var bool|string + */ + public static $forceRefresh = false; + + /** + * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up + * + * @var int + */ + public static $gcLifetime = 604800; + + /** + * array of already refreshed cache if $forceRefresh==='once' + * + * @var array + */ + protected static $refreshed = []; + + /** + * Constructor + * + * @param array $options + * + * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options + */ + public function __construct($options) + { + // check $cacheDir + if (isset($options['cacheDir'])) { + self::$cacheDir = $options['cacheDir']; + } + + if (empty(self::$cacheDir)) { + throw new Exception('cacheDir not set'); + } + + if (isset($options['prefix'])) { + self::$prefix = $options['prefix']; + } + + if (empty(self::$prefix)) { + throw new Exception('prefix not set'); + } + + if (isset($options['forceRefresh'])) { + self::$forceRefresh = $options['forceRefresh']; + } + + self::checkCacheDir(); + } + + /** + * Get the cached result of $operation on $what, + * which is known as dependant from the content of $options + * + * @param string $operation parse, compile... + * @param mixed $what content key (e.g., filename to be treated) + * @param array $options any option that affect the operation result on the content + * @param int|null $lastModified last modified timestamp + * + * @return mixed + * + * @throws \Exception + */ + public function getCache($operation, $what, $options = [], $lastModified = null) + { + $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); + + if ( + ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' && + isset(self::$refreshed[$fileCache]))) && file_exists($fileCache) + ) { + $cacheTime = filemtime($fileCache); + + if ( + (\is_null($lastModified) || $cacheTime > $lastModified) && + $cacheTime + self::$gcLifetime > time() + ) { + $c = file_get_contents($fileCache); + $c = unserialize($c); + + if (\is_array($c) && isset($c['value'])) { + return $c['value']; + } + } + } + + return null; + } + + /** + * Put in cache the result of $operation on $what, + * which is known as dependant from the content of $options + * + * @param string $operation + * @param mixed $what + * @param mixed $value + * @param array $options + * + * @return void + */ + public function setCache($operation, $what, $value, $options = []) + { + $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); + + $c = ['value' => $value]; + $c = serialize($c); + + file_put_contents($fileCache, $c); + + if (self::$forceRefresh === 'once') { + self::$refreshed[$fileCache] = true; + } + } + + /** + * Get the cache name for the caching of $operation on $what, + * which is known as dependant from the content of $options + * + * @param string $operation + * @param mixed $what + * @param array $options + * + * @return string + */ + private static function cacheName($operation, $what, $options = []) + { + $t = [ + 'version' => self::CACHE_VERSION, + 'scssphpVersion' => Version::VERSION, + 'operation' => $operation, + 'what' => $what, + 'options' => $options + ]; + + $t = self::$prefix + . sha1(json_encode($t)) + . ".$operation" + . ".scsscache"; + + return $t; + } + + /** + * Check that the cache dir exists and is writeable + * + * @return void + * + * @throws \Exception + */ + public static function checkCacheDir() + { + self::$cacheDir = str_replace('\\', '/', self::$cacheDir); + self::$cacheDir = rtrim(self::$cacheDir, '/') . '/'; + + if (! is_dir(self::$cacheDir)) { + throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir); + } + + if (! is_writable(self::$cacheDir)) { + throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir); + } + } + + /** + * Delete unused cached files + * + * @return void + */ + public static function cleanCache() + { + static $clean = false; + + if ($clean || empty(self::$cacheDir)) { + return; + } + + $clean = true; + + // only remove files with extensions created by SCSSPHP Cache + // css files removed based on the list files + $removeTypes = ['scsscache' => 1]; + + $files = scandir(self::$cacheDir); + + if (! $files) { + return; + } + + $checkTime = time() - self::$gcLifetime; + + foreach ($files as $file) { + // don't delete if the file wasn't created with SCSSPHP Cache + if (strpos($file, self::$prefix) !== 0) { + continue; + } + + $parts = explode('.', $file); + $type = array_pop($parts); + + if (! isset($removeTypes[$type])) { + continue; + } + + $fullPath = self::$cacheDir . $file; + $mtime = filemtime($fullPath); + + // don't delete if it's a relatively new file + if ($mtime > $checkTime) { + continue; + } + + unlink($fullPath); + } + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Colors.php b/plugins/admin/vendor/scssphp/scssphp/src/Colors.php new file mode 100644 index 0000000..2df3999 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Colors.php @@ -0,0 +1,247 @@ + + * + * @internal + */ +class Colors +{ + /** + * CSS Colors + * + * @see http://www.w3.org/TR/css3-color + * + * @var array + */ + protected static $cssColors = [ + 'aliceblue' => '240,248,255', + 'antiquewhite' => '250,235,215', + 'aqua' => '0,255,255', + 'cyan' => '0,255,255', + 'aquamarine' => '127,255,212', + 'azure' => '240,255,255', + 'beige' => '245,245,220', + 'bisque' => '255,228,196', + 'black' => '0,0,0', + 'blanchedalmond' => '255,235,205', + 'blue' => '0,0,255', + 'blueviolet' => '138,43,226', + 'brown' => '165,42,42', + 'burlywood' => '222,184,135', + 'cadetblue' => '95,158,160', + 'chartreuse' => '127,255,0', + 'chocolate' => '210,105,30', + 'coral' => '255,127,80', + 'cornflowerblue' => '100,149,237', + 'cornsilk' => '255,248,220', + 'crimson' => '220,20,60', + 'darkblue' => '0,0,139', + 'darkcyan' => '0,139,139', + 'darkgoldenrod' => '184,134,11', + 'darkgray' => '169,169,169', + 'darkgrey' => '169,169,169', + 'darkgreen' => '0,100,0', + 'darkkhaki' => '189,183,107', + 'darkmagenta' => '139,0,139', + 'darkolivegreen' => '85,107,47', + 'darkorange' => '255,140,0', + 'darkorchid' => '153,50,204', + 'darkred' => '139,0,0', + 'darksalmon' => '233,150,122', + 'darkseagreen' => '143,188,143', + 'darkslateblue' => '72,61,139', + 'darkslategray' => '47,79,79', + 'darkslategrey' => '47,79,79', + 'darkturquoise' => '0,206,209', + 'darkviolet' => '148,0,211', + 'deeppink' => '255,20,147', + 'deepskyblue' => '0,191,255', + 'dimgray' => '105,105,105', + 'dimgrey' => '105,105,105', + 'dodgerblue' => '30,144,255', + 'firebrick' => '178,34,34', + 'floralwhite' => '255,250,240', + 'forestgreen' => '34,139,34', + 'fuchsia' => '255,0,255', + 'magenta' => '255,0,255', + 'gainsboro' => '220,220,220', + 'ghostwhite' => '248,248,255', + 'gold' => '255,215,0', + 'goldenrod' => '218,165,32', + 'gray' => '128,128,128', + 'grey' => '128,128,128', + 'green' => '0,128,0', + 'greenyellow' => '173,255,47', + 'honeydew' => '240,255,240', + 'hotpink' => '255,105,180', + 'indianred' => '205,92,92', + 'indigo' => '75,0,130', + 'ivory' => '255,255,240', + 'khaki' => '240,230,140', + 'lavender' => '230,230,250', + 'lavenderblush' => '255,240,245', + 'lawngreen' => '124,252,0', + 'lemonchiffon' => '255,250,205', + 'lightblue' => '173,216,230', + 'lightcoral' => '240,128,128', + 'lightcyan' => '224,255,255', + 'lightgoldenrodyellow' => '250,250,210', + 'lightgray' => '211,211,211', + 'lightgrey' => '211,211,211', + 'lightgreen' => '144,238,144', + 'lightpink' => '255,182,193', + 'lightsalmon' => '255,160,122', + 'lightseagreen' => '32,178,170', + 'lightskyblue' => '135,206,250', + 'lightslategray' => '119,136,153', + 'lightslategrey' => '119,136,153', + 'lightsteelblue' => '176,196,222', + 'lightyellow' => '255,255,224', + 'lime' => '0,255,0', + 'limegreen' => '50,205,50', + 'linen' => '250,240,230', + 'maroon' => '128,0,0', + 'mediumaquamarine' => '102,205,170', + 'mediumblue' => '0,0,205', + 'mediumorchid' => '186,85,211', + 'mediumpurple' => '147,112,219', + 'mediumseagreen' => '60,179,113', + 'mediumslateblue' => '123,104,238', + 'mediumspringgreen' => '0,250,154', + 'mediumturquoise' => '72,209,204', + 'mediumvioletred' => '199,21,133', + 'midnightblue' => '25,25,112', + 'mintcream' => '245,255,250', + 'mistyrose' => '255,228,225', + 'moccasin' => '255,228,181', + 'navajowhite' => '255,222,173', + 'navy' => '0,0,128', + 'oldlace' => '253,245,230', + 'olive' => '128,128,0', + 'olivedrab' => '107,142,35', + 'orange' => '255,165,0', + 'orangered' => '255,69,0', + 'orchid' => '218,112,214', + 'palegoldenrod' => '238,232,170', + 'palegreen' => '152,251,152', + 'paleturquoise' => '175,238,238', + 'palevioletred' => '219,112,147', + 'papayawhip' => '255,239,213', + 'peachpuff' => '255,218,185', + 'peru' => '205,133,63', + 'pink' => '255,192,203', + 'plum' => '221,160,221', + 'powderblue' => '176,224,230', + 'purple' => '128,0,128', + 'red' => '255,0,0', + 'rosybrown' => '188,143,143', + 'royalblue' => '65,105,225', + 'saddlebrown' => '139,69,19', + 'salmon' => '250,128,114', + 'sandybrown' => '244,164,96', + 'seagreen' => '46,139,87', + 'seashell' => '255,245,238', + 'sienna' => '160,82,45', + 'silver' => '192,192,192', + 'skyblue' => '135,206,235', + 'slateblue' => '106,90,205', + 'slategray' => '112,128,144', + 'slategrey' => '112,128,144', + 'snow' => '255,250,250', + 'springgreen' => '0,255,127', + 'steelblue' => '70,130,180', + 'tan' => '210,180,140', + 'teal' => '0,128,128', + 'thistle' => '216,191,216', + 'tomato' => '255,99,71', + 'turquoise' => '64,224,208', + 'violet' => '238,130,238', + 'wheat' => '245,222,179', + 'white' => '255,255,255', + 'whitesmoke' => '245,245,245', + 'yellow' => '255,255,0', + 'yellowgreen' => '154,205,50', + 'rebeccapurple' => '102,51,153', + 'transparent' => '0,0,0,0', + ]; + + /** + * Convert named color in a [r,g,b[,a]] array + * + * @param string $colorName + * + * @return int[]|null + */ + public static function colorNameToRGBa($colorName) + { + if (\is_string($colorName) && isset(static::$cssColors[$colorName])) { + $rgba = explode(',', static::$cssColors[$colorName]); + + // only case with opacity is transparent, with opacity=0, so we can intval on opacity also + $rgba = array_map('intval', $rgba); + + return $rgba; + } + + return null; + } + + /** + * Reverse conversion : from RGBA to a color name if possible + * + * @param int $r + * @param int $g + * @param int $b + * @param int|float $a + * + * @return string|null + */ + public static function RGBaToColorName($r, $g, $b, $a = 1) + { + static $reverseColorTable = null; + + if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) { + return null; + } + + if ($a < 1) { + return null; + } + + if (\is_null($reverseColorTable)) { + $reverseColorTable = []; + + foreach (static::$cssColors as $name => $rgb_str) { + $rgb_str = explode(',', $rgb_str); + + if ( + \count($rgb_str) == 3 && + ! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])]) + ) { + $reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name; + } + } + } + + if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) { + return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)]; + } + + return null; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php b/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php new file mode 100644 index 0000000..36adb0d --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php @@ -0,0 +1,69 @@ +css = $css; + $this->sourceMap = $sourceMap; + $this->includedFiles = $includedFiles; + } + + /** + * @return string + */ + public function getCss() + { + return $this->css; + } + + /** + * @return string[] + */ + public function getIncludedFiles() + { + return $this->includedFiles; + } + + /** + * The sourceMap content, if it was generated + * + * @return null|string + */ + public function getSourceMap() + { + return $this->sourceMap; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php b/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php new file mode 100644 index 0000000..ecafc8c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php @@ -0,0 +1,10430 @@ + + * + * @final Extending the Compiler is deprecated + */ +class Compiler +{ + /** + * @deprecated + */ + const LINE_COMMENTS = 1; + /** + * @deprecated + */ + const DEBUG_INFO = 2; + + /** + * @deprecated + */ + const WITH_RULE = 1; + /** + * @deprecated + */ + const WITH_MEDIA = 2; + /** + * @deprecated + */ + const WITH_SUPPORTS = 4; + /** + * @deprecated + */ + const WITH_ALL = 7; + + const SOURCE_MAP_NONE = 0; + const SOURCE_MAP_INLINE = 1; + const SOURCE_MAP_FILE = 2; + + /** + * @var array + */ + protected static $operatorNames = [ + '+' => 'add', + '-' => 'sub', + '*' => 'mul', + '/' => 'div', + '%' => 'mod', + + '==' => 'eq', + '!=' => 'neq', + '<' => 'lt', + '>' => 'gt', + + '<=' => 'lte', + '>=' => 'gte', + ]; + + /** + * @var array + */ + protected static $namespaces = [ + 'special' => '%', + 'mixin' => '@', + 'function' => '^', + ]; + + public static $true = [Type::T_KEYWORD, 'true']; + public static $false = [Type::T_KEYWORD, 'false']; + /** @deprecated */ + public static $NaN = [Type::T_KEYWORD, 'NaN']; + /** @deprecated */ + public static $Infinity = [Type::T_KEYWORD, 'Infinity']; + public static $null = [Type::T_NULL]; + public static $nullString = [Type::T_STRING, '', []]; + public static $defaultValue = [Type::T_KEYWORD, '']; + public static $selfSelector = [Type::T_SELF]; + public static $emptyList = [Type::T_LIST, '', []]; + public static $emptyMap = [Type::T_MAP, [], []]; + public static $emptyString = [Type::T_STRING, '"', []]; + public static $with = [Type::T_KEYWORD, 'with']; + public static $without = [Type::T_KEYWORD, 'without']; + private static $emptyArgumentList = [Type::T_LIST, '', [], []]; + + /** + * @var array + */ + protected $importPaths = []; + /** + * @var array + */ + protected $importCache = []; + + /** + * @var string[] + */ + protected $importedFiles = []; + + /** + * @var array + * @phpstan-var array + */ + protected $userFunctions = []; + /** + * @var array + */ + protected $registeredVars = []; + /** + * @var array + */ + protected $registeredFeatures = [ + 'extend-selector-pseudoclass' => false, + 'at-error' => true, + 'units-level-3' => true, + 'global-variable-shadowing' => false, + ]; + + /** + * @var string|null + */ + protected $encoding = null; + /** + * @var null + * @deprecated + */ + protected $lineNumberStyle = null; + + /** + * @var int|SourceMapGenerator + * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator + */ + protected $sourceMap = self::SOURCE_MAP_NONE; + + /** + * @var array + * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} + */ + protected $sourceMapOptions = []; + + /** + * @var bool + */ + private $charset = true; + + /** + * @var Formatter + */ + protected $formatter; + + /** + * @var string + * @phpstan-var class-string + */ + private $configuredFormatter = Expanded::class; + + /** + * @var Environment + */ + protected $rootEnv; + /** + * @var OutputBlock|null + */ + protected $rootBlock; + + /** + * @var \ScssPhp\ScssPhp\Compiler\Environment + */ + protected $env; + /** + * @var OutputBlock|null + */ + protected $scope; + /** + * @var Environment|null + */ + protected $storeEnv; + /** + * @var bool|null + * + * @deprecated + */ + protected $charsetSeen; + /** + * @var array + */ + protected $sourceNames; + + /** + * @var Cache|null + */ + protected $cache; + + /** + * @var bool + */ + protected $cacheCheckImportResolutions = false; + + /** + * @var int + */ + protected $indentLevel; + /** + * @var array[] + */ + protected $extends; + /** + * @var array + */ + protected $extendsMap; + + /** + * @var array + */ + protected $parsedFiles = []; + + /** + * @var Parser|null + */ + protected $parser; + /** + * @var int|null + */ + protected $sourceIndex; + /** + * @var int|null + */ + protected $sourceLine; + /** + * @var int|null + */ + protected $sourceColumn; + /** + * @var bool|null + */ + protected $shouldEvaluate; + /** + * @var null + * @deprecated + */ + protected $ignoreErrors; + /** + * @var bool + */ + protected $ignoreCallStackMessage = false; + + /** + * @var array[] + */ + protected $callStack = []; + + /** + * @var array + * @phpstan-var list + */ + private $resolvedImports = []; + + /** + * The directory of the currently processed file + * + * @var string|null + */ + private $currentDirectory; + + /** + * The directory of the input file + * + * @var string + */ + private $rootDirectory; + + /** + * @var bool + */ + private $legacyCwdImportPath = true; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var array + */ + private $warnedChildFunctions = []; + + /** + * Constructor + * + * @param array|null $cacheOptions + * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions + */ + public function __construct($cacheOptions = null) + { + $this->sourceNames = []; + + if ($cacheOptions) { + $this->cache = new Cache($cacheOptions); + if (!empty($cacheOptions['checkImportResolutions'])) { + $this->cacheCheckImportResolutions = true; + } + } + + $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true); + } + + /** + * Get compiler options + * + * @return array + * + * @internal + */ + public function getCompileOptions() + { + $options = [ + 'importPaths' => $this->importPaths, + 'registeredVars' => $this->registeredVars, + 'registeredFeatures' => $this->registeredFeatures, + 'encoding' => $this->encoding, + 'sourceMap' => serialize($this->sourceMap), + 'sourceMapOptions' => $this->sourceMapOptions, + 'formatter' => $this->configuredFormatter, + 'legacyImportPath' => $this->legacyCwdImportPath, + ]; + + return $options; + } + + /** + * Sets an alternative logger. + * + * Changing the logger in the middle of the compilation is not + * supported and will result in an undefined behavior. + * + * @param LoggerInterface $logger + * + * @return void + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Set an alternative error output stream, for testing purpose only + * + * @param resource $handle + * + * @return void + * + * @deprecated Use {@see setLogger} instead + */ + public function setErrorOuput($handle) + { + @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED); + + $this->logger = new StreamLogger($handle); + } + + /** + * Compile scss + * + * @param string $code + * @param string|null $path + * + * @return string + * + * @throws SassException when the source fails to compile + * + * @deprecated Use {@see compileString} instead. + */ + public function compile($code, $path = null) + { + @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED); + + $result = $this->compileString($code, $path); + + $sourceMap = $result->getSourceMap(); + + if ($sourceMap !== null) { + if ($this->sourceMap instanceof SourceMapGenerator) { + $this->sourceMap->saveMap($sourceMap); + } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + $sourceMapGenerator->saveMap($sourceMap); + } + } + + return $result->getCss(); + } + + /** + * Compile scss + * + * @param string $source + * @param string|null $path + * + * @return CompilationResult + * + * @throws SassException when the source fails to compile + */ + public function compileString($source, $path = null) + { + if ($this->cache) { + $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source); + $compileOptions = $this->getCompileOptions(); + $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions); + + if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) { + return $cachedResult->getResult(); + } + } + + $this->indentLevel = -1; + $this->extends = []; + $this->extendsMap = []; + $this->sourceIndex = null; + $this->sourceLine = null; + $this->sourceColumn = null; + $this->env = null; + $this->scope = null; + $this->storeEnv = null; + $this->shouldEvaluate = null; + $this->ignoreCallStackMessage = false; + $this->parsedFiles = []; + $this->importedFiles = []; + $this->resolvedImports = []; + + if (!\is_null($path) && is_file($path)) { + $path = realpath($path) ?: $path; + $this->currentDirectory = dirname($path); + $this->rootDirectory = $this->currentDirectory; + } else { + $this->currentDirectory = null; + $this->rootDirectory = getcwd(); + } + + try { + $this->parser = $this->parserFactory($path); + $tree = $this->parser->parse($source); + $this->parser = null; + + $this->formatter = new $this->configuredFormatter(); + $this->rootBlock = null; + $this->rootEnv = $this->pushEnv($tree); + + $warnCallback = function ($message, $deprecation) { + $this->logger->warn($message, $deprecation); + }; + $previousWarnCallback = Warn::setCallback($warnCallback); + + try { + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); + } finally { + Warn::setCallback($previousWarnCallback); + } + + $sourceMapGenerator = null; + + if ($this->sourceMap) { + if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { + $sourceMapGenerator = $this->sourceMap; + $this->sourceMap = self::SOURCE_MAP_FILE; + } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + } + } + assert($this->scope !== null); + + $out = $this->formatter->format($this->scope, $sourceMapGenerator); + + $prefix = ''; + + if ($this->charset && strlen($out) !== Util::mbStrlen($out)) { + $prefix = '@charset "UTF-8";' . "\n"; + $out = $prefix . $out; + } + + $sourceMap = null; + + if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { + assert($sourceMapGenerator !== null); + $sourceMap = $sourceMapGenerator->generateJson($prefix); + $sourceMapUrl = null; + + switch ($this->sourceMap) { + case self::SOURCE_MAP_INLINE: + $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); + break; + + case self::SOURCE_MAP_FILE: + if (isset($this->sourceMapOptions['sourceMapURL'])) { + $sourceMapUrl = $this->sourceMapOptions['sourceMapURL']; + } + break; + } + + if ($sourceMapUrl !== null) { + $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); + } + } + } catch (SassScriptException $e) { + throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e); + } + + $includedFiles = []; + + foreach ($this->resolvedImports as $resolvedImport) { + $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath']; + } + + $result = new CompilationResult($out, $sourceMap, array_values($includedFiles)); + + if ($this->cache && isset($cacheKey) && isset($compileOptions)) { + $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions); + } + + // Reset state to free memory + // TODO in 2.0, reset parsedFiles as well when the getter is removed. + $this->resolvedImports = []; + $this->importedFiles = []; + + return $result; + } + + /** + * @param CachedResult $result + * + * @return bool + */ + private function isFreshCachedResult(CachedResult $result) + { + // check if any dependency file changed since the result was compiled + foreach ($result->getParsedFiles() as $file => $mtime) { + if (! is_file($file) || filemtime($file) !== $mtime) { + return false; + } + } + + if ($this->cacheCheckImportResolutions) { + $resolvedImports = []; + + foreach ($result->getResolvedImports() as $import) { + $currentDir = $import['currentDir']; + $path = $import['path']; + // store the check across all the results in memory to avoid multiple findImport() on the same path + // with same context. + // this is happening in a same hit with multiple compilations (especially with big frameworks) + if (empty($resolvedImports[$currentDir][$path])) { + $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir); + } + + if ($resolvedImports[$currentDir][$path] !== $import['filePath']) { + return false; + } + } + } + + return true; + } + + /** + * Instantiate parser + * + * @param string|null $path + * + * @return \ScssPhp\ScssPhp\Parser + */ + protected function parserFactory($path) + { + // https://sass-lang.com/documentation/at-rules/import + // CSS files imported by Sass don’t allow any special Sass features. + // In order to make sure authors don’t accidentally write Sass in their CSS, + // all Sass features that aren’t also valid CSS will produce errors. + // Otherwise, the CSS will be rendered as-is. It can even be extended! + $cssOnly = false; + + if ($path !== null && substr($path, -4) === '.css') { + $cssOnly = true; + } + + $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger); + + $this->sourceNames[] = $path; + $this->addParsedFile($path); + + return $parser; + } + + /** + * Is self extend? + * + * @param array $target + * @param array $origin + * + * @return bool + */ + protected function isSelfExtend($target, $origin) + { + foreach ($origin as $sel) { + if (\in_array($target, $sel)) { + return true; + } + } + + return false; + } + + /** + * Push extends + * + * @param string[] $target + * @param array $origin + * @param array|null $block + * + * @return void + */ + protected function pushExtends($target, $origin, $block) + { + $i = \count($this->extends); + $this->extends[] = [$target, $origin, $block]; + + foreach ($target as $part) { + if (isset($this->extendsMap[$part])) { + $this->extendsMap[$part][] = $i; + } else { + $this->extendsMap[$part] = [$i]; + } + } + } + + /** + * Make output block + * + * @param string|null $type + * @param string[]|null $selectors + * + * @return \ScssPhp\ScssPhp\Formatter\OutputBlock + */ + protected function makeOutputBlock($type, $selectors = null) + { + $out = new OutputBlock(); + $out->type = $type; + $out->lines = []; + $out->children = []; + $out->parent = $this->scope; + $out->selectors = $selectors; + $out->depth = $this->env->depth; + + if ($this->env->block instanceof Block) { + $out->sourceName = $this->env->block->sourceName; + $out->sourceLine = $this->env->block->sourceLine; + $out->sourceColumn = $this->env->block->sourceColumn; + } else { + $out->sourceName = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '(stdin)'; + $out->sourceLine = $this->sourceLine; + $out->sourceColumn = $this->sourceColumn; + } + + return $out; + } + + /** + * Compile root + * + * @param \ScssPhp\ScssPhp\Block $rootBlock + * + * @return void + */ + protected function compileRoot(Block $rootBlock) + { + $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); + + $this->compileChildrenNoReturn($rootBlock->children, $this->scope); + assert($this->scope !== null); + $this->flattenSelectors($this->scope); + $this->missingSelectors(); + } + + /** + * Report missing selectors + * + * @return void + */ + protected function missingSelectors() + { + foreach ($this->extends as $extend) { + if (isset($extend[3])) { + continue; + } + + list($target, $origin, $block) = $extend; + + // ignore if !optional + if ($block[2]) { + continue; + } + + $target = implode(' ', $target); + $origin = $this->collapseSelectors($origin); + + $this->sourceLine = $block[Parser::SOURCE_LINE]; + throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); + } + } + + /** + * Flatten selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * @param string $parentKey + * + * @return void + */ + protected function flattenSelectors(OutputBlock $block, $parentKey = null) + { + if ($block->selectors) { + $selectors = []; + + foreach ($block->selectors as $s) { + $selectors[] = $s; + + if (! \is_array($s)) { + continue; + } + + // check extends + if (! empty($this->extendsMap)) { + $this->matchExtends($s, $selectors); + + // remove duplicates + array_walk($selectors, function (&$value) { + $value = serialize($value); + }); + + $selectors = array_unique($selectors); + + array_walk($selectors, function (&$value) { + $value = unserialize($value); + }); + } + } + + $block->selectors = []; + $placeholderSelector = false; + + foreach ($selectors as $selector) { + if ($this->hasSelectorPlaceholder($selector)) { + $placeholderSelector = true; + continue; + } + + $block->selectors[] = $this->compileSelector($selector); + } + + if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) { + assert($block->parent !== null); + unset($block->parent->children[$parentKey]); + + return; + } + } + + foreach ($block->children as $key => $child) { + $this->flattenSelectors($child, $key); + } + } + + /** + * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts + * + * @param array $parts + * + * @return array + */ + protected function glueFunctionSelectors($parts) + { + $new = []; + + foreach ($parts as $part) { + if (\is_array($part)) { + $part = $this->glueFunctionSelectors($part); + $new[] = $part; + } else { + // a selector part finishing with a ) is the last part of a :not( or :nth-child( + // and need to be joined to this + if ( + \count($new) && \is_string($new[\count($new) - 1]) && + \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false + ) { + while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') { + $part = array_pop($new) . $part; + } + $new[\count($new) - 1] .= $part; + } else { + $new[] = $part; + } + } + } + + return $new; + } + + /** + * Match extends + * + * @param array $selector + * @param array $out + * @param int $from + * @param bool $initial + * + * @return void + */ + protected function matchExtends($selector, &$out, $from = 0, $initial = true) + { + static $partsPile = []; + $selector = $this->glueFunctionSelectors($selector); + + if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) { + return; + } + + $outRecurs = []; + + foreach ($selector as $i => $part) { + if ($i < $from) { + continue; + } + + // check that we are not building an infinite loop of extensions + // if the new part is just including a previous part don't try to extend anymore + if (\count($part) > 1) { + foreach ($partsPile as $previousPart) { + if (! \count(array_diff($previousPart, $part))) { + continue 2; + } + } + } + + $partsPile[] = $part; + + if ($this->matchExtendsSingle($part, $origin, $initial)) { + $after = \array_slice($selector, $i + 1); + $before = \array_slice($selector, 0, $i); + list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); + + foreach ($origin as $new) { + $k = 0; + + // remove shared parts + if (\count($new) > 1) { + while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) { + $k++; + } + } + + if (\count($nonBreakableBefore) && $k === \count($new)) { + $k--; + } + + $replacement = []; + $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new; + + for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) { + $slice = []; + + foreach ($tempReplacement[$l] as $chunk) { + if (! \in_array($chunk, $slice)) { + $slice[] = $chunk; + } + } + + array_unshift($replacement, $slice); + + if (! $this->isImmediateRelationshipCombinator(end($slice))) { + break; + } + } + + $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : []; + + // Merge shared direct relationships. + $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore); + + $result = array_merge( + $before, + $mergedBefore, + $replacement, + $after + ); + + if ($result === $selector) { + continue; + } + + $this->pushOrMergeExtentedSelector($out, $result); + + // recursively check for more matches + $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore)); + + if (\count($origin) > 1) { + $this->matchExtends($result, $out, $startRecurseFrom, false); + } else { + $this->matchExtends($result, $outRecurs, $startRecurseFrom, false); + } + + // selector sequence merging + if (! empty($before) && \count($new) > 1) { + $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : []; + $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before; + + list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore); + + $result2 = array_merge( + $preSharedParts, + $betweenSharedParts, + $postSharedParts, + $nonBreakabl2, + $nonBreakableBefore, + $replacement, + $after + ); + + $this->pushOrMergeExtentedSelector($out, $result2); + } + } + } + array_pop($partsPile); + } + + while (\count($outRecurs)) { + $result = array_shift($outRecurs); + $this->pushOrMergeExtentedSelector($out, $result); + } + } + + /** + * Test a part for being a pseudo selector + * + * @param string $part + * @param array $matches + * + * @return bool + */ + protected function isPseudoSelector($part, &$matches) + { + if ( + strpos($part, ':') === 0 && + preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches) + ) { + return true; + } + + return false; + } + + /** + * Push extended selector except if + * - this is a pseudo selector + * - same as previous + * - in a white list + * in this case we merge the pseudo selector content + * + * @param array $out + * @param array $extended + * + * @return void + */ + protected function pushOrMergeExtentedSelector(&$out, $extended) + { + if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) { + $single = reset($extended); + $part = reset($single); + + if ( + $this->isPseudoSelector($part, $matchesExtended) && + \in_array($matchesExtended[1], [ 'slotted' ]) + ) { + $prev = end($out); + $prev = $this->glueFunctionSelectors($prev); + + if (\count($prev) === 1 && \count(reset($prev)) === 1) { + $single = reset($prev); + $part = reset($single); + + if ( + $this->isPseudoSelector($part, $matchesPrev) && + $matchesPrev[1] === $matchesExtended[1] + ) { + $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2); + $extended[1] = $matchesPrev[2] . ', ' . $extended[1]; + $extended = implode($matchesExtended[1] . '(', $extended); + $extended = [ [ $extended ]]; + array_pop($out); + } + } + } + } + $out[] = $extended; + } + + /** + * Match extends single + * + * @param array $rawSingle + * @param array $outOrigin + * @param bool $initial + * + * @return bool + */ + protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) + { + $counts = []; + $single = []; + + // simple usual cases, no need to do the whole trick + if (\in_array($rawSingle, [['>'],['+'],['~']])) { + return false; + } + + foreach ($rawSingle as $part) { + // matches Number + if (! \is_string($part)) { + return false; + } + + if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) { + $single[\count($single) - 1] .= $part; + } else { + $single[] = $part; + } + } + + $extendingDecoratedTag = false; + + if (\count($single) > 1) { + $matches = null; + $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false; + } + + $outOrigin = []; + $found = false; + + foreach ($single as $k => $part) { + if (isset($this->extendsMap[$part])) { + foreach ($this->extendsMap[$part] as $idx) { + $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; + } + } + + if ( + $initial && + $this->isPseudoSelector($part, $matches) && + ! \in_array($matches[1], [ 'not' ]) + ) { + $buffer = $matches[2]; + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseSelector($buffer, $subSelectors, false)) { + foreach ($subSelectors as $ksub => $subSelector) { + $subExtended = []; + $this->matchExtends($subSelector, $subExtended, 0, false); + + if ($subExtended) { + $subSelectorsExtended = $subSelectors; + $subSelectorsExtended[$ksub] = $subExtended; + + foreach ($subSelectorsExtended as $ksse => $sse) { + $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse); + } + + $subSelectorsExtended = implode(', ', $subSelectorsExtended); + $singleExtended = $single; + $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part); + $outOrigin[] = [ $singleExtended ]; + $found = true; + } + } + } + } + } + + foreach ($counts as $idx => $count) { + list($target, $origin, /* $block */) = $this->extends[$idx]; + + $origin = $this->glueFunctionSelectors($origin); + + // check count + if ($count !== \count($target)) { + continue; + } + + $this->extends[$idx][3] = true; + + $rem = array_diff($single, $target); + + foreach ($origin as $j => $new) { + // prevent infinite loop when target extends itself + if ($this->isSelfExtend($single, $origin) && ! $initial) { + return false; + } + + $replacement = end($new); + + // Extending a decorated tag with another tag is not possible. + if ( + $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag && + preg_match('/^[a-z0-9]+$/i', $replacement[0]) + ) { + unset($origin[$j]); + continue; + } + + $combined = $this->combineSelectorSingle($replacement, $rem); + + if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) { + $origin[$j][\count($origin[$j]) - 1] = $combined; + } + } + + $outOrigin = array_merge($outOrigin, $origin); + + $found = true; + } + + return $found; + } + + /** + * Extract a relationship from the fragment. + * + * When extracting the last portion of a selector we will be left with a + * fragment which may end with a direction relationship combinator. This + * method will extract the relationship fragment and return it along side + * the rest. + * + * @param array $fragment The selector fragment maybe ending with a direction relationship combinator. + * + * @return array The selector without the relationship fragment if any, the relationship fragment. + */ + protected function extractRelationshipFromFragment(array $fragment) + { + $parents = []; + $children = []; + + $j = $i = \count($fragment); + + for (;;) { + $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : []; + $parents = \array_slice($fragment, 0, $j); + $slice = end($parents); + + if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) { + break; + } + + $j -= 2; + } + + return [$parents, $children]; + } + + /** + * Combine selector single + * + * @param array $base + * @param array $other + * + * @return array + */ + protected function combineSelectorSingle($base, $other) + { + $tag = []; + $out = []; + $wasTag = false; + $pseudo = []; + + while (\count($other) && strpos(end($other), ':') === 0) { + array_unshift($pseudo, array_pop($other)); + } + + foreach ([array_reverse($base), array_reverse($other)] as $single) { + $rang = count($single); + + foreach ($single as $part) { + if (preg_match('/^[\[:]/', $part)) { + $out[] = $part; + $wasTag = false; + } elseif (preg_match('/^[\.#]/', $part)) { + array_unshift($out, $part); + $wasTag = false; + } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) { + $tag[] = $part; + $wasTag = true; + } elseif ($wasTag) { + $tag[\count($tag) - 1] .= $part; + } else { + array_unshift($out, $part); + } + $rang--; + } + } + + if (\count($tag)) { + array_unshift($out, $tag[0]); + } + + while (\count($pseudo)) { + $out[] = array_shift($pseudo); + } + + return $out; + } + + /** + * Compile media + * + * @param \ScssPhp\ScssPhp\Block $media + * + * @return void + */ + protected function compileMedia(Block $media) + { + assert($media instanceof MediaBlock); + $this->pushEnv($media); + + $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); + + if (! empty($mediaQueries)) { + assert($this->scope !== null); + $previousScope = $this->scope; + $parentScope = $this->mediaParent($this->scope); + + foreach ($mediaQueries as $mediaQuery) { + $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]); + + $parentScope->children[] = $this->scope; + $parentScope = $this->scope; + } + + // top level properties in a media cause it to be wrapped + $needsWrap = false; + + foreach ($media->children as $child) { + $type = $child[0]; + + if ( + $type !== Type::T_BLOCK && + $type !== Type::T_MEDIA && + $type !== Type::T_DIRECTIVE && + $type !== Type::T_IMPORT + ) { + $needsWrap = true; + break; + } + } + + if ($needsWrap) { + $wrapped = new Block(); + $wrapped->sourceName = $media->sourceName; + $wrapped->sourceIndex = $media->sourceIndex; + $wrapped->sourceLine = $media->sourceLine; + $wrapped->sourceColumn = $media->sourceColumn; + $wrapped->selectors = []; + $wrapped->comments = []; + $wrapped->parent = $media; + $wrapped->children = $media->children; + + $media->children = [[Type::T_BLOCK, $wrapped]]; + } + + $this->compileChildrenNoReturn($media->children, $this->scope); + + $this->scope = $previousScope; + } + + $this->popEnv(); + } + + /** + * Media parent + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * + * @return \ScssPhp\ScssPhp\Formatter\OutputBlock + */ + protected function mediaParent(OutputBlock $scope) + { + while (! empty($scope->parent)) { + if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) { + break; + } + + $scope = $scope->parent; + } + + return $scope; + } + + /** + * Compile directive + * + * @param DirectiveBlock|array $directive + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void + */ + protected function compileDirective($directive, OutputBlock $out) + { + if (\is_array($directive)) { + $directiveName = $this->compileDirectiveName($directive[0]); + $s = '@' . $directiveName; + + if (! empty($directive[1])) { + $s .= ' ' . $this->compileValue($directive[1]); + } + // sass-spec compliance on newline after directives, a bit tricky :/ + $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : ""; + if (\is_array($directive[0]) && empty($directive[1])) { + $appendNewLine = "\n"; + } + + if (empty($directive[3])) { + $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]); + } else { + $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';'); + } + } else { + $directive->name = $this->compileDirectiveName($directive->name); + $s = '@' . $directive->name; + + if (! empty($directive->value)) { + $s .= ' ' . $this->compileValue($directive->value); + } + + if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') { + $this->compileKeyframeBlock($directive, [$s]); + } else { + $this->compileNestedBlock($directive, [$s]); + } + } + } + + /** + * directive names can include some interpolation + * + * @param string|array $directiveName + * @return string + * @throws CompilerException + */ + protected function compileDirectiveName($directiveName) + { + if (is_string($directiveName)) { + return $directiveName; + } + + return $this->compileValue($directiveName); + } + + /** + * Compile at-root + * + * @param \ScssPhp\ScssPhp\Block $block + * + * @return void + */ + protected function compileAtRoot(Block $block) + { + assert($block instanceof AtRootBlock); + $env = $this->pushEnv($block); + $envs = $this->compactEnv($env); + list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null); + + // wrap inline selector + if ($block->selector) { + $wrapped = new Block(); + $wrapped->sourceName = $block->sourceName; + $wrapped->sourceIndex = $block->sourceIndex; + $wrapped->sourceLine = $block->sourceLine; + $wrapped->sourceColumn = $block->sourceColumn; + $wrapped->selectors = $block->selector; + $wrapped->comments = []; + $wrapped->parent = $block; + $wrapped->children = $block->children; + $wrapped->selfParent = $block->selfParent; + + $block->children = [[Type::T_BLOCK, $wrapped]]; + $block->selector = null; + } + + $selfParent = $block->selfParent; + assert($selfParent !== null, 'at-root blocks must have a selfParent set.'); + + if ( + ! $selfParent->selectors && + isset($block->parent) && + isset($block->parent->selectors) && $block->parent->selectors + ) { + $selfParent = $block->parent; + } + + $this->env = $this->filterWithWithout($envs, $with, $without); + + assert($this->scope !== null); + $saveScope = $this->scope; + $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without); + + // propagate selfParent to the children where they still can be useful + $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent); + + assert($this->scope !== null); + $this->completeScope($this->scope, $saveScope); + $this->scope = $saveScope; + $this->env = $this->extractEnv($envs); + + $this->popEnv(); + } + + /** + * Filter at-root scope depending on with/without option + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param array $with + * @param array $without + * + * @return OutputBlock + */ + protected function filterScopeWithWithout($scope, $with, $without) + { + $filteredScopes = []; + $childStash = []; + + if ($scope->type === Type::T_ROOT) { + return $scope; + } + assert($this->rootBlock !== null); + + // start from the root + while ($scope->parent && $scope->parent->type !== Type::T_ROOT) { + array_unshift($childStash, $scope); + $scope = $scope->parent; + } + + for (;;) { + if (! $scope) { + break; + } + + if ($this->isWith($scope, $with, $without)) { + $s = clone $scope; + $s->children = []; + $s->lines = []; + $s->parent = null; + + if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) { + $s->selectors = []; + } + + $filteredScopes[] = $s; + } + + if (\count($childStash)) { + $scope = array_shift($childStash); + } elseif ($scope->children) { + $scope = end($scope->children); + } else { + $scope = null; + } + } + + if (! \count($filteredScopes)) { + return $this->rootBlock; + } + + $newScope = array_shift($filteredScopes); + $newScope->parent = $this->rootBlock; + + $this->rootBlock->children[] = $newScope; + + $p = &$newScope; + + while (\count($filteredScopes)) { + $s = array_shift($filteredScopes); + $s->parent = $p; + $p->children[] = $s; + $newScope = &$p->children[0]; + $p = &$p->children[0]; + } + + return $newScope; + } + + /** + * found missing selector from a at-root compilation in the previous scope + * (if at-root is just enclosing a property, the selector is in the parent tree) + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope + * + * @return OutputBlock + */ + protected function completeScope($scope, $previousScope) + { + if (! $scope->type && ! $scope->selectors && \count($scope->lines)) { + $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); + } + + if ($scope->children) { + foreach ($scope->children as $k => $c) { + $scope->children[$k] = $this->completeScope($c, $previousScope); + } + } + + return $scope; + } + + /** + * Find a selector by the depth node in the scope + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param int $depth + * + * @return array + */ + protected function findScopeSelectors($scope, $depth) + { + if ($scope->depth === $depth && $scope->selectors) { + return $scope->selectors; + } + + if ($scope->children) { + foreach (array_reverse($scope->children) as $c) { + if ($s = $this->findScopeSelectors($c, $depth)) { + return $s; + } + } + } + + return []; + } + + /** + * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later + * + * @param array|null $withCondition + * + * @return array + * + * @phpstan-return array{array, array} + */ + protected function compileWith($withCondition) + { + // just compile what we have in 2 lists + $with = []; + $without = ['rule' => true]; + + if ($withCondition) { + if ($withCondition[0] === Type::T_INTERPOLATE) { + $w = $this->compileValue($withCondition); + + $buffer = "($w)"; + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseValue($buffer, $reParsedWith)) { + $withCondition = $reParsedWith; + } + } + + $withConfig = $this->mapGet($withCondition, static::$with); + if ($withConfig !== null) { + $without = []; // cancel the default + $list = $this->coerceList($withConfig); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + $with[$keyword] = true; + } + } + + $withoutConfig = $this->mapGet($withCondition, static::$without); + if ($withoutConfig !== null) { + $without = []; // cancel the default + $list = $this->coerceList($withoutConfig); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + $without[$keyword] = true; + } + } + } + + return [$with, $without]; + } + + /** + * Filter env stack + * + * @param Environment[] $envs + * @param array $with + * @param array $without + * + * @return Environment + * + * @phpstan-param non-empty-array $envs + */ + protected function filterWithWithout($envs, $with, $without) + { + $filtered = []; + + foreach ($envs as $e) { + if ($e->block && ! $this->isWith($e->block, $with, $without)) { + $ec = clone $e; + $ec->block = null; + $ec->selectors = []; + + $filtered[] = $ec; + } else { + $filtered[] = $e; + } + } + + return $this->extractEnv($filtered); + } + + /** + * Filter WITH rules + * + * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block + * @param array $with + * @param array $without + * + * @return bool + */ + protected function isWith($block, $with, $without) + { + if (isset($block->type)) { + if ($block->type === Type::T_MEDIA) { + return $this->testWithWithout('media', $with, $without); + } + + if ($block->type === Type::T_DIRECTIVE) { + assert($block instanceof DirectiveBlock || $block instanceof OutputBlock); + if (isset($block->name)) { + return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without); + } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) { + return $this->testWithWithout($m[1], $with, $without); + } else { + return $this->testWithWithout('???', $with, $without); + } + } + } elseif (isset($block->selectors)) { + // a selector starting with number is a keyframe rule + if (\count($block->selectors)) { + $s = reset($block->selectors); + + while (\is_array($s)) { + $s = reset($s); + } + + if (\is_object($s) && $s instanceof Number) { + return $this->testWithWithout('keyframes', $with, $without); + } + } + + return $this->testWithWithout('rule', $with, $without); + } + + return true; + } + + /** + * Test a single type of block against with/without lists + * + * @param string $what + * @param array $with + * @param array $without + * + * @return bool + * true if the block should be kept, false to reject + */ + protected function testWithWithout($what, $with, $without) + { + // if without, reject only if in the list (or 'all' is in the list) + if (\count($without)) { + return (isset($without[$what]) || isset($without['all'])) ? false : true; + } + + // otherwise reject all what is not in the with list + return (isset($with[$what]) || isset($with['all'])) ? true : false; + } + + + /** + * Compile keyframe block + * + * @param \ScssPhp\ScssPhp\Block $block + * @param string[] $selectors + * + * @return void + */ + protected function compileKeyframeBlock(Block $block, $selectors) + { + $env = $this->pushEnv($block); + + $envs = $this->compactEnv($env); + + $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) { + return ! isset($e->block->selectors); + })); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->depth = 1; + assert($this->scope->parent !== null); + $this->scope->parent->children[] = $this->scope; + + $this->compileChildrenNoReturn($block->children, $this->scope); + + assert($this->scope !== null); + $this->scope = $this->scope->parent; + $this->env = $this->extractEnv($envs); + + $this->popEnv(); + } + + /** + * Compile nested properties lines + * + * @param \ScssPhp\ScssPhp\Block $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void + */ + protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) + { + assert($block instanceof NestedPropertyBlock); + $prefix = $this->compileValue($block->prefix) . '-'; + + $nested = $this->makeOutputBlock($block->type); + $nested->parent = $out; + + if ($block->hasValue) { + $nested->depth = $out->depth + 1; + } + + $out->children[] = $nested; + + foreach ($block->children as $child) { + switch ($child[0]) { + case Type::T_ASSIGN: + array_unshift($child[1][2], $prefix); + break; + + case Type::T_NESTED_PROPERTY: + assert($child[1] instanceof NestedPropertyBlock); + array_unshift($child[1]->prefix[2], $prefix); + break; + } + + $this->compileChild($child, $nested); + } + } + + /** + * Compile nested block + * + * @param \ScssPhp\ScssPhp\Block $block + * @param string[] $selectors + * + * @return void + */ + protected function compileNestedBlock(Block $block, $selectors) + { + $this->pushEnv($block); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + assert($this->scope->parent !== null); + $this->scope->parent->children[] = $this->scope; + + // wrap assign children in a block + // except for @font-face + if (!$block instanceof DirectiveBlock || $this->compileDirectiveName($block->name) !== 'font-face') { + // need wrapping? + $needWrapping = false; + + foreach ($block->children as $child) { + if ($child[0] === Type::T_ASSIGN) { + $needWrapping = true; + break; + } + } + + if ($needWrapping) { + $wrapped = new Block(); + $wrapped->sourceName = $block->sourceName; + $wrapped->sourceIndex = $block->sourceIndex; + $wrapped->sourceLine = $block->sourceLine; + $wrapped->sourceColumn = $block->sourceColumn; + $wrapped->selectors = []; + $wrapped->comments = []; + $wrapped->parent = $block; + $wrapped->children = $block->children; + $wrapped->selfParent = $block->selfParent; + + $block->children = [[Type::T_BLOCK, $wrapped]]; + } + } + + $this->compileChildrenNoReturn($block->children, $this->scope); + + assert($this->scope !== null); + $this->scope = $this->scope->parent; + + $this->popEnv(); + } + + /** + * Recursively compiles a block. + * + * A block is analogous to a CSS block in most cases. A single SCSS document + * is encapsulated in a block when parsed, but it does not have parent tags + * so all of its children appear on the root level when compiled. + * + * Blocks are made up of selectors and children. + * + * The children of a block are just all the blocks that are defined within. + * + * Compiling the block involves pushing a fresh environment on the stack, + * and iterating through the props, compiling each one. + * + * @see Compiler::compileChild() + * + * @param \ScssPhp\ScssPhp\Block $block + * + * @return void + */ + protected function compileBlock(Block $block) + { + $env = $this->pushEnv($block); + assert($block->selectors !== null); + $env->selectors = $this->evalSelectors($block->selectors); + + $out = $this->makeOutputBlock(null); + + assert($this->scope !== null); + $this->scope->children[] = $out; + + if (\count($block->children)) { + $out->selectors = $this->multiplySelectors($env, $block->selfParent); + + // propagate selfParent to the children where they still can be useful + $selfParentSelectors = null; + + if (isset($block->selfParent->selectors)) { + $selfParentSelectors = $block->selfParent->selectors; + $block->selfParent->selectors = $out->selectors; + } + + $this->compileChildrenNoReturn($block->children, $out, $block->selfParent); + + // and revert for the following children of the same block + if ($selfParentSelectors) { + assert($block->selfParent !== null); + $block->selfParent->selectors = $selfParentSelectors; + } + } + + $this->popEnv(); + } + + + /** + * Compile the value of a comment that can have interpolation + * + * @param array $value + * @param bool $pushEnv + * + * @return string + */ + protected function compileCommentValue($value, $pushEnv = false) + { + $c = $value[1]; + + if (isset($value[2])) { + if ($pushEnv) { + $this->pushEnv(); + } + + try { + $c = $this->compileValue($value[2]); + } catch (SassScriptException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true); + // ignore error in comment compilation which are only interpolation + } catch (SassException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true); + // ignore error in comment compilation which are only interpolation + } + + if ($pushEnv) { + $this->popEnv(); + } + } + + return $c; + } + + /** + * Compile root level comment + * + * @param array $block + * + * @return void + */ + protected function compileComment($block) + { + $out = $this->makeOutputBlock(Type::T_COMMENT); + $out->lines[] = $this->compileCommentValue($block, true); + + assert($this->scope !== null); + $this->scope->children[] = $out; + } + + /** + * Evaluate selectors + * + * @param array $selectors + * + * @return array + */ + protected function evalSelectors($selectors) + { + $this->shouldEvaluate = false; + + $evaluatedSelectors = []; + foreach ($selectors as $selector) { + $evaluatedSelectors[] = $this->evalSelector($selector); + } + $selectors = $evaluatedSelectors; + + // after evaluating interpolates, we might need a second pass + if ($this->shouldEvaluate) { + $selectors = $this->replaceSelfSelector($selectors, '&'); + $buffer = $this->collapseSelectors($selectors); + $parser = $this->parserFactory(__METHOD__); + + try { + $isValid = $parser->parseSelector($buffer, $newSelectors, true); + } catch (ParserException $e) { + throw $this->error($e->getMessage()); + } + + if ($isValid) { + $selectors = array_map([$this, 'evalSelector'], $newSelectors); + } + } + + return $selectors; + } + + /** + * Evaluate selector + * + * @param array $selector + * + * @return array + * + * @phpstan-impure + */ + protected function evalSelector($selector) + { + return array_map([$this, 'evalSelectorPart'], $selector); + } + + /** + * Evaluate selector part; replaces all the interpolates, stripping quotes + * + * @param array $part + * + * @return array + * + * @phpstan-impure + */ + protected function evalSelectorPart($part) + { + foreach ($part as &$p) { + if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { + $p = $this->compileValue($p); + + // force re-evaluation if self char or non standard char + if (preg_match(',[^\w-],', $p)) { + $this->shouldEvaluate = true; + } + } elseif ( + \is_string($p) && \strlen($p) >= 2 && + ($p[0] === '"' || $p[0] === "'") && + substr($p, -1) === $p[0] + ) { + $p = substr($p, 1, -1); + } + } + + return $this->flattenSelectorSingle($part); + } + + /** + * Collapse selectors + * + * @param array $selectors + * + * @return string + */ + protected function collapseSelectors($selectors) + { + $parts = []; + + foreach ($selectors as $selector) { + $output = []; + + foreach ($selector as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + $output[] = $compound; + } + + $parts[] = implode(' ', $output); + } + + return implode(', ', $parts); + } + + /** + * Collapse selectors + * + * @param array $selectors + * + * @return array + */ + private function collapseSelectorsAsList($selectors) + { + $parts = []; + + foreach ($selectors as $selector) { + $output = []; + $glueNext = false; + + foreach ($selector as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + if ($this->isImmediateRelationshipCombinator($compound)) { + if (\count($output)) { + $output[\count($output) - 1] .= ' ' . $compound; + } else { + $output[] = $compound; + } + + $glueNext = true; + } elseif ($glueNext) { + $output[\count($output) - 1] .= ' ' . $compound; + $glueNext = false; + } else { + $output[] = $compound; + } + } + + foreach ($output as &$o) { + $o = [Type::T_STRING, '', [$o]]; + } + + $parts[] = [Type::T_LIST, ' ', $output]; + } + + return [Type::T_LIST, ',', $parts]; + } + + /** + * Parse down the selector and revert [self] to "&" before a reparsing + * + * @param array $selectors + * @param string|null $replace + * + * @return array + */ + protected function replaceSelfSelector($selectors, $replace = null) + { + foreach ($selectors as &$part) { + if (\is_array($part)) { + if ($part === [Type::T_SELF]) { + if (\is_null($replace)) { + $replace = $this->reduce([Type::T_SELF]); + $replace = $this->compileValue($replace); + } + $part = $replace; + } else { + $part = $this->replaceSelfSelector($part, $replace); + } + } + } + + return $selectors; + } + + /** + * Flatten selector single; joins together .classes and #ids + * + * @param array $single + * + * @return array + */ + protected function flattenSelectorSingle($single) + { + $joined = []; + + foreach ($single as $part) { + if ( + empty($joined) || + ! \is_string($part) || + preg_match('/[\[.:#%]/', $part) + ) { + $joined[] = $part; + continue; + } + + if (\is_array(end($joined))) { + $joined[] = $part; + } else { + $joined[\count($joined) - 1] .= $part; + } + } + + return $joined; + } + + /** + * Compile selector to string; self(&) should have been replaced by now + * + * @param string|array $selector + * + * @return string + */ + protected function compileSelector($selector) + { + if (! \is_array($selector)) { + return $selector; // media and the like + } + + return implode( + ' ', + array_map( + [$this, 'compileSelectorPart'], + $selector + ) + ); + } + + /** + * Compile selector part + * + * @param array $piece + * + * @return string + */ + protected function compileSelectorPart($piece) + { + foreach ($piece as &$p) { + if (! \is_array($p)) { + continue; + } + + switch ($p[0]) { + case Type::T_SELF: + $p = '&'; + break; + + default: + $p = $this->compileValue($p); + break; + } + } + + return implode($piece); + } + + /** + * Has selector placeholder? + * + * @param array $selector + * + * @return bool + */ + protected function hasSelectorPlaceholder($selector) + { + if (! \is_array($selector)) { + return false; + } + + foreach ($selector as $parts) { + foreach ($parts as $part) { + if (\strlen($part) && '%' === $part[0]) { + return true; + } + } + } + + return false; + } + + /** + * @param string $name + * + * @return void + */ + protected function pushCallStack($name = '') + { + $this->callStack[] = [ + 'n' => $name, + Parser::SOURCE_INDEX => $this->sourceIndex, + Parser::SOURCE_LINE => $this->sourceLine, + Parser::SOURCE_COLUMN => $this->sourceColumn + ]; + + // infinite calling loop + if (\count($this->callStack) > 25000) { + // not displayed but you can var_dump it to deep debug + $msg = $this->callStackMessage(true, 100); + $msg = 'Infinite calling loop'; + + throw $this->error($msg); + } + } + + /** + * @return void + */ + protected function popCallStack() + { + array_pop($this->callStack); + } + + /** + * Compile children and return result + * + * @param array $stms + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param string $traceName + * + * @return array|Number|null + */ + protected function compileChildren($stms, OutputBlock $out, $traceName = '') + { + $this->pushCallStack($traceName); + + foreach ($stms as $stm) { + $ret = $this->compileChild($stm, $out); + + if (isset($ret)) { + $this->popCallStack(); + + return $ret; + } + } + + $this->popCallStack(); + + return null; + } + + /** + * Compile children and throw exception if unexpected at-return + * + * @param array[] $stms + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param \ScssPhp\ScssPhp\Block $selfParent + * @param string $traceName + * + * @return void + * + * @throws \Exception + */ + protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') + { + $this->pushCallStack($traceName); + + foreach ($stms as $stm) { + if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) { + $oldSelfParent = $stm[1]->selfParent; + $stm[1]->selfParent = $selfParent; + $ret = $this->compileChild($stm, $out); + $stm[1]->selfParent = $oldSelfParent; + } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) { + $stm['selfParent'] = $selfParent; + $ret = $this->compileChild($stm, $out); + } else { + $ret = $this->compileChild($stm, $out); + } + + if (isset($ret)) { + throw $this->error('@return may only be used within a function'); + } + } + + $this->popCallStack(); + } + + + /** + * evaluate media query : compile internal value keeping the structure unchanged + * + * @param array $queryList + * + * @return array + */ + protected function evaluateMediaQuery($queryList) + { + static $parser = null; + + $outQueryList = []; + + foreach ($queryList as $kql => $query) { + $shouldReparse = false; + + foreach ($query as $kq => $q) { + for ($i = 1; $i < \count($q); $i++) { + $value = $this->compileValue($q[$i]); + + // the parser had no mean to know if media type or expression if it was an interpolation + // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type + if ( + $q[0] == Type::T_MEDIA_TYPE && + (strpos($value, '(') !== false || + strpos($value, ')') !== false || + strpos($value, ':') !== false || + strpos($value, ',') !== false) + ) { + $shouldReparse = true; + } + + $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value]; + } + } + + if ($shouldReparse) { + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $queryString = $this->compileMediaQuery([$queryList[$kql]]); + $queryString = reset($queryString); + + if ($queryString !== false && strpos($queryString, '@media ') === 0) { + $queryString = substr($queryString, 7); + $queries = []; + + if ($parser->parseMediaQueryList($queryString, $queries)) { + $queries = $this->evaluateMediaQuery($queries[2]); + + while (\count($queries)) { + $outQueryList[] = array_shift($queries); + } + + continue; + } + } + } + + $outQueryList[] = $queryList[$kql]; + } + + return $outQueryList; + } + + /** + * Compile media query + * + * @param array $queryList + * + * @return string[] + */ + protected function compileMediaQuery($queryList) + { + $start = '@media '; + $default = trim($start); + $out = []; + $current = ''; + + foreach ($queryList as $query) { + $type = null; + $parts = []; + + $mediaTypeOnly = true; + + foreach ($query as $q) { + if ($q[0] !== Type::T_MEDIA_TYPE) { + $mediaTypeOnly = false; + break; + } + } + + foreach ($query as $q) { + switch ($q[0]) { + case Type::T_MEDIA_TYPE: + $newType = array_map([$this, 'compileValue'], \array_slice($q, 1)); + + // combining not and anything else than media type is too risky and should be avoided + if (! $mediaTypeOnly) { + if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) { + if ($type) { + array_unshift($parts, implode(' ', array_filter($type))); + } + + if (! empty($parts)) { + if (\strlen($current)) { + $current .= $this->formatter->tagSeparator; + } + + $current .= implode(' and ', $parts); + } + + if ($current) { + $out[] = $start . $current; + } + + $current = ''; + $type = null; + $parts = []; + } + } + + if ($newType === ['all'] && $default) { + $default = $start . 'all'; + } + + // all can be safely ignored and mixed with whatever else + if ($newType !== ['all']) { + if ($type) { + $type = $this->mergeMediaTypes($type, $newType); + + if (empty($type)) { + // merge failed : ignore this query that is not valid, skip to the next one + $parts = []; + $default = ''; // if everything fail, no @media at all + continue 3; + } + } else { + $type = $newType; + } + } + break; + + case Type::T_MEDIA_EXPRESSION: + if (isset($q[2])) { + $parts[] = '(' + . $this->compileValue($q[1]) + . $this->formatter->assignSeparator + . $this->compileValue($q[2]) + . ')'; + } else { + $parts[] = '(' + . $this->compileValue($q[1]) + . ')'; + } + break; + + case Type::T_MEDIA_VALUE: + $parts[] = $this->compileValue($q[1]); + break; + } + } + + if ($type) { + array_unshift($parts, implode(' ', array_filter($type))); + } + + if (! empty($parts)) { + if (\strlen($current)) { + $current .= $this->formatter->tagSeparator; + } + + $current .= implode(' and ', $parts); + } + } + + if ($current) { + $out[] = $start . $current; + } + + // no @media type except all, and no conflict? + if (! $out && $default) { + $out[] = $default; + } + + return $out; + } + + /** + * Merge direct relationships between selectors + * + * @param array $selectors1 + * @param array $selectors2 + * + * @return array + */ + protected function mergeDirectRelationships($selectors1, $selectors2) + { + if (empty($selectors1) || empty($selectors2)) { + return array_merge($selectors1, $selectors2); + } + + $part1 = end($selectors1); + $part2 = end($selectors2); + + if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { + return array_merge($selectors1, $selectors2); + } + + $merged = []; + + do { + $part1 = array_pop($selectors1); + $part2 = array_pop($selectors2); + + if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { + if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) { + array_unshift($merged, [$part1[0] . $part2[0]]); + $merged = array_merge($selectors1, $selectors2, $merged); + } else { + $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged); + } + + break; + } + + array_unshift($merged, $part1); + } while (! empty($selectors1) && ! empty($selectors2)); + + return $merged; + } + + /** + * Merge media types + * + * @param array $type1 + * @param array $type2 + * + * @return array|null + */ + protected function mergeMediaTypes($type1, $type2) + { + if (empty($type1)) { + return $type2; + } + + if (empty($type2)) { + return $type1; + } + + if (\count($type1) > 1) { + $m1 = strtolower($type1[0]); + $t1 = strtolower($type1[1]); + } else { + $m1 = ''; + $t1 = strtolower($type1[0]); + } + + if (\count($type2) > 1) { + $m2 = strtolower($type2[0]); + $t2 = strtolower($type2[1]); + } else { + $m2 = ''; + $t2 = strtolower($type2[0]); + } + + if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) { + if ($t1 === $t2) { + return null; + } + + return [ + $m1 === Type::T_NOT ? $m2 : $m1, + $m1 === Type::T_NOT ? $t2 : $t1, + ]; + } + + if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) { + // CSS has no way of representing "neither screen nor print" + if ($t1 !== $t2) { + return null; + } + + return [Type::T_NOT, $t1]; + } + + if ($t1 !== $t2) { + return null; + } + + // t1 == t2, neither m1 nor m2 are "not" + return [empty($m1) ? $m2 : $m1, $t1]; + } + + /** + * Compile import; returns true if the value was something that could be imported + * + * @param array $rawPath + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param bool $once + * + * @return bool + */ + protected function compileImport($rawPath, OutputBlock $out, $once = false) + { + if ($rawPath[0] === Type::T_STRING) { + $path = $this->compileStringContent($rawPath); + + if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) { + $this->registerImport($this->currentDirectory, $path, $filePath); + + if (! $once || ! \in_array($filePath, $this->importedFiles)) { + $this->importFile($filePath, $out); + $this->importedFiles[] = $filePath; + } + + return true; + } + + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + + return false; + } + + if ($rawPath[0] === Type::T_LIST) { + // handle a list of strings + if (\count($rawPath[2]) === 0) { + return false; + } + + foreach ($rawPath[2] as $path) { + if ($path[0] !== Type::T_STRING) { + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + + return false; + } + } + + foreach ($rawPath[2] as $path) { + $this->compileImport($path, $out, $once); + } + + return true; + } + + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + + return false; + } + + /** + * @param array $rawPath + * @return string + * @throws CompilerException + */ + protected function compileImportPath($rawPath) + { + $path = $this->compileValue($rawPath); + + // case url() without quotes : suppress \r \n remaining in the path + // if this is a real string there can not be CR or LF char + if (strpos($path, 'url(') === 0) { + $path = str_replace(array("\r", "\n"), array('', ' '), $path); + } else { + // if this is a file name in a string, spaces should be escaped + $path = $this->reduce($rawPath); + $path = $this->escapeImportPathString($path); + $path = $this->compileValue($path); + } + + return $path; + } + + /** + * @param array $path + * @return array + * @throws CompilerException + */ + protected function escapeImportPathString($path) + { + switch ($path[0]) { + case Type::T_LIST: + foreach ($path[2] as $k => $v) { + $path[2][$k] = $this->escapeImportPathString($v); + } + break; + case Type::T_STRING: + if ($path[1]) { + $path = $this->compileValue($path); + $path = str_replace(' ', '\\ ', $path); + $path = [Type::T_KEYWORD, $path]; + } + break; + } + + return $path; + } + + /** + * Append a root directive like @import or @charset as near as the possible from the source code + * (keeping before comments, @import and @charset coming before in the source code) + * + * @param string $line + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param array $allowed + * + * @return void + */ + protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) + { + $root = $out; + + while ($root->parent) { + $root = $root->parent; + } + + $i = 0; + + while ($i < \count($root->children)) { + if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) { + break; + } + + $i++; + } + + // remove incompatible children from the bottom of the list + $saveChildren = []; + + while ($i < \count($root->children)) { + $saveChildren[] = array_pop($root->children); + } + + // insert the directive as a comment + $child = $this->makeOutputBlock(Type::T_COMMENT); + $child->lines[] = $line; + $child->sourceName = $this->sourceNames[$this->sourceIndex] ?: '(stdin)'; + $child->sourceLine = $this->sourceLine; + $child->sourceColumn = $this->sourceColumn; + + $root->children[] = $child; + + // repush children + while (\count($saveChildren)) { + $root->children[] = array_pop($saveChildren); + } + } + + /** + * Append lines to the current output block: + * directly to the block or through a child if necessary + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param string $type + * @param string $line + * + * @return void + */ + protected function appendOutputLine(OutputBlock $out, $type, $line) + { + $outWrite = &$out; + + // check if it's a flat output or not + if (\count($out->children)) { + $lastChild = &$out->children[\count($out->children) - 1]; + + if ( + $lastChild->depth === $out->depth && + \is_null($lastChild->selectors) && + ! \count($lastChild->children) + ) { + $outWrite = $lastChild; + } else { + $nextLines = $this->makeOutputBlock($type); + $nextLines->parent = $out; + $nextLines->depth = $out->depth; + + $out->children[] = $nextLines; + $outWrite = &$nextLines; + } + } + + $outWrite->lines[] = $line; + } + + /** + * Compile child; returns a value to halt execution + * + * @param array $child + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return array|Number|null + */ + protected function compileChild($child, OutputBlock $out) + { + if (isset($child[Parser::SOURCE_LINE])) { + $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; + $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; + $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; + } elseif (\is_array($child) && isset($child[1]->sourceLine) && $child[1] instanceof Block) { + $this->sourceIndex = $child[1]->sourceIndex; + $this->sourceLine = $child[1]->sourceLine; + $this->sourceColumn = $child[1]->sourceColumn; + } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { + $this->sourceLine = $out->sourceLine; + $sourceIndex = array_search($out->sourceName, $this->sourceNames); + $this->sourceColumn = $out->sourceColumn; + + if ($sourceIndex === false) { + $sourceIndex = null; + } + $this->sourceIndex = $sourceIndex; + } + + switch ($child[0]) { + case Type::T_SCSSPHP_IMPORT_ONCE: + $rawPath = $this->reduce($child[1]); + + $this->compileImport($rawPath, $out, true); + break; + + case Type::T_IMPORT: + $rawPath = $this->reduce($child[1]); + + $this->compileImport($rawPath, $out); + break; + + case Type::T_DIRECTIVE: + $this->compileDirective($child[1], $out); + break; + + case Type::T_AT_ROOT: + $this->compileAtRoot($child[1]); + break; + + case Type::T_MEDIA: + $this->compileMedia($child[1]); + break; + + case Type::T_BLOCK: + $this->compileBlock($child[1]); + break; + + case Type::T_CHARSET: + break; + + case Type::T_CUSTOM_PROPERTY: + list(, $name, $value) = $child; + $compiledName = $this->compileValue($name); + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] !== Type::T_NULL) { + $value = $this->reduce($value); + + if ($value[0] === Type::T_NULL || $value === static::$nullString) { + break; + } + } + + $compiledValue = $this->compileValue($value); + + $line = $this->formatter->customProperty( + $compiledName, + $compiledValue + ); + + $this->appendOutputLine($out, Type::T_ASSIGN, $line); + break; + + case Type::T_ASSIGN: + list(, $name, $value) = $child; + + if ($name[0] === Type::T_VARIABLE) { + $flags = isset($child[3]) ? $child[3] : []; + $isDefault = \in_array('!default', $flags); + $isGlobal = \in_array('!global', $flags); + + if ($isGlobal) { + $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value); + break; + } + + $shouldSet = $isDefault && + (\is_null($result = $this->get($name[1], false)) || + $result === static::$null); + + if (! $isDefault || $shouldSet) { + $this->set($name[1], $this->reduce($value), true, null, $value); + } + break; + } + + $compiledName = $this->compileValue($name); + + // handle shorthand syntaxes : size / line-height... + if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) { + if ($value[0] === Type::T_VARIABLE) { + // if the font value comes from variable, the content is already reduced + // (i.e., formulas were already calculated), so we need the original unreduced value + $value = $this->get($value[1], true, null, true); + } + + $shorthandValue=&$value; + + $shorthandDividerNeedsUnit = false; + $maxListElements = null; + $maxShorthandDividers = 1; + + switch ($compiledName) { + case 'border-radius': + $maxListElements = 4; + $shorthandDividerNeedsUnit = true; + break; + } + + if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') { + // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica" + // we need to handle the first list element + $shorthandValue=&$value[2][0]; + } + + if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') { + $revert = true; + + if ($shorthandDividerNeedsUnit) { + $divider = $shorthandValue[3]; + + if (\is_array($divider)) { + $divider = $this->reduce($divider, true); + } + + if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + $revert = false; + } + } + + if ($revert) { + $shorthandValue = $this->expToString($shorthandValue); + } + } elseif ($shorthandValue[0] === Type::T_LIST) { + foreach ($shorthandValue[2] as &$item) { + if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { + if ($maxShorthandDividers > 0) { + $revert = true; + + // if the list of values is too long, this has to be a shorthand, + // otherwise it could be a real division + if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) { + if ($shorthandDividerNeedsUnit) { + $divider = $item[3]; + + if (\is_array($divider)) { + $divider = $this->reduce($divider, true); + } + + if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + $revert = false; + } + } + } + + if ($revert) { + $item = $this->expToString($item); + $maxShorthandDividers--; + } + } + } + } + } + } + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] !== Type::T_NULL) { + $value = $this->reduce($value); + + if ($value[0] === Type::T_NULL || $value === static::$nullString) { + break; + } + } + + $compiledValue = $this->compileValue($value); + + // ignore empty value + if (\strlen($compiledValue)) { + $line = $this->formatter->property( + $compiledName, + $compiledValue + ); + $this->appendOutputLine($out, Type::T_ASSIGN, $line); + } + break; + + case Type::T_COMMENT: + if ($out->type === Type::T_ROOT) { + $this->compileComment($child); + break; + } + + $line = $this->compileCommentValue($child, true); + $this->appendOutputLine($out, Type::T_COMMENT, $line); + break; + + case Type::T_MIXIN: + case Type::T_FUNCTION: + list(, $block) = $child; + assert($block instanceof CallableBlock); + // the block need to be able to go up to it's parent env to resolve vars + $block->parentEnv = $this->getStoreEnv(); + $this->set(static::$namespaces[$block->type] . $block->name, $block, true); + break; + + case Type::T_EXTEND: + foreach ($child[1] as $sel) { + $replacedSel = $this->replaceSelfSelector($sel); + + if ($replacedSel !== $sel) { + throw $this->error('Parent selectors aren\'t allowed here.'); + } + + $results = $this->evalSelectors([$sel]); + + foreach ($results as $result) { + if (\count($result) !== 1) { + throw $this->error('complex selectors may not be extended.'); + } + + // only use the first one + $result = $result[0]; + $selectors = $out->selectors; + + if (! $selectors && isset($child['selfParent'])) { + $selectors = $this->multiplySelectors($this->env, $child['selfParent']); + } + assert($selectors !== null); + + if (\count($result) > 1) { + $replacement = implode(', ', $result); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + $message = <<logger->warn($message); + } + + $this->pushExtends($result, $selectors, $child); + } + } + break; + + case Type::T_IF: + list(, $if) = $child; + assert($if instanceof IfBlock); + + if ($this->isTruthy($this->reduce($if->cond, true))) { + return $this->compileChildren($if->children, $out); + } + + foreach ($if->cases as $case) { + if ( + $case instanceof ElseBlock || + $case instanceof ElseifBlock && $this->isTruthy($this->reduce($case->cond)) + ) { + return $this->compileChildren($case->children, $out); + } + } + break; + + case Type::T_EACH: + list(, $each) = $child; + assert($each instanceof EachBlock); + + $list = $this->coerceList($this->reduce($each->list), ',', true); + + $this->pushEnv(); + + foreach ($list[2] as $item) { + if (\count($each->vars) === 1) { + $this->set($each->vars[0], $item, true); + } else { + list(,, $values) = $this->coerceList($item); + + foreach ($each->vars as $i => $var) { + $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true); + } + } + + $ret = $this->compileChildren($each->children, $out); + + if ($ret) { + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, $each->vars); + + return $ret; + } + } + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, $each->vars); + + break; + + case Type::T_WHILE: + list(, $while) = $child; + assert($while instanceof WhileBlock); + + while ($this->isTruthy($this->reduce($while->cond, true))) { + $ret = $this->compileChildren($while->children, $out); + + if ($ret) { + return $ret; + } + } + break; + + case Type::T_FOR: + list(, $for) = $child; + assert($for instanceof ForBlock); + + $startNumber = $this->assertNumber($this->reduce($for->start, true)); + $endNumber = $this->assertNumber($this->reduce($for->end, true)); + + $start = $this->assertInteger($startNumber); + + $numeratorUnits = $startNumber->getNumeratorUnits(); + $denominatorUnits = $startNumber->getDenominatorUnits(); + + $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits)); + + $d = $start < $end ? 1 : -1; + + $this->pushEnv(); + + for (;;) { + if ( + (! $for->until && $start - $d == $end) || + ($for->until && $start == $end) + ) { + break; + } + + $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits)); + $start += $d; + + $ret = $this->compileChildren($for->children, $out); + + if ($ret) { + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, [$for->var]); + + return $ret; + } + } + + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, [$for->var]); + + break; + + case Type::T_RETURN: + return $this->reduce($child[1], true); + + case Type::T_NESTED_PROPERTY: + $this->compileNestedPropertiesBlock($child[1], $out); + break; + + case Type::T_INCLUDE: + // including a mixin + list(, $name, $argValues, $content, $argUsing) = $child; + + $mixin = $this->get(static::$namespaces['mixin'] . $name, false); + + if (! $mixin) { + throw $this->error("Undefined mixin $name"); + } + + assert($mixin instanceof CallableBlock); + + $callingScope = $this->getStoreEnv(); + + // push scope, apply args + $this->pushEnv(); + $this->env->depth--; + + // Find the parent selectors in the env to be able to know what '&' refers to in the mixin + // and assign this fake parent to childs + $selfParent = null; + + if (isset($child['selfParent']) && $child['selfParent'] instanceof Block && isset($child['selfParent']->selectors)) { + $selfParent = $child['selfParent']; + } else { + $parentSelectors = $this->multiplySelectors($this->env); + + if ($parentSelectors) { + $parent = new Block(); + $parent->selectors = $parentSelectors; + + foreach ($mixin->children as $k => $child) { + if (isset($child[1]) && $child[1] instanceof Block) { + $mixin->children[$k][1]->parent = $parent; + } + } + } + } + + // clone the stored content to not have its scope spoiled by a further call to the same mixin + // i.e., recursive @include of the same mixin + if (isset($content)) { + $copyContent = clone $content; + $copyContent->scope = clone $callingScope; + + $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env); + } else { + $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env); + } + + // save the "using" argument list for applying it to when "@content" is invoked + if (isset($argUsing)) { + $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env); + } else { + $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env); + } + + if (isset($mixin->args)) { + $this->applyArguments($mixin->args, $argValues); + } + + $this->env->marker = 'mixin'; + + if (! empty($mixin->parentEnv)) { + $this->env->declarationScopeParent = $mixin->parentEnv; + } else { + throw $this->error("@mixin $name() without parentEnv"); + } + + $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name); + + $this->popEnv(); + break; + + case Type::T_MIXIN_CONTENT: + $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; + $content = $this->get(static::$namespaces['special'] . 'content', false, $env); + $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); + $argContent = $child[1]; + + if (! $content) { + break; + } + + $storeEnv = $this->storeEnv; + $varsUsing = []; + + if (isset($argUsing) && isset($argContent)) { + // Get the arguments provided for the content with the names provided in the "using" argument list + $this->storeEnv = null; + $varsUsing = $this->applyArguments($argUsing, $argContent, false); + } + + // restore the scope from the @content + $this->storeEnv = $content->scope; + + // append the vars from using if any + foreach ($varsUsing as $name => $val) { + $this->set($name, $val, true, $this->storeEnv); + } + + $this->compileChildrenNoReturn($content->children, $out); + + $this->storeEnv = $storeEnv; + break; + + case Type::T_DEBUG: + list(, $value) = $child; + + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileDebugValue($value); + + $this->logger->debug("$fname:$line DEBUG: $value"); + break; + + case Type::T_WARN: + list(, $value) = $child; + + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileDebugValue($value); + + $this->logger->warn("$value\n on line $line of $fname"); + break; + + case Type::T_ERROR: + list(, $value) = $child; + + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileValue($this->reduce($value, true)); + + throw $this->error("File $fname on line $line ERROR: $value\n"); + + default: + throw $this->error("unknown child type: $child[0]"); + } + + return null; + } + + /** + * Reduce expression to string + * + * @param array $exp + * @param bool $keepParens + * + * @return array + */ + protected function expToString($exp, $keepParens = false) + { + list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; + + $content = []; + + if ($keepParens && $inParens) { + $content[] = '('; + } + + $content[] = $this->reduce($left); + + if ($whiteLeft) { + $content[] = ' '; + } + + $content[] = $op; + + if ($whiteRight) { + $content[] = ' '; + } + + $content[] = $this->reduce($right); + + if ($keepParens && $inParens) { + $content[] = ')'; + } + + return [Type::T_STRING, '', $content]; + } + + /** + * Is truthy? + * + * @param array|Number $value + * + * @return bool + */ + public function isTruthy($value) + { + return $value !== static::$false && $value !== static::$null; + } + + /** + * Is the value a direct relationship combinator? + * + * @param string $value + * + * @return bool + */ + protected function isImmediateRelationshipCombinator($value) + { + return $value === '>' || $value === '+' || $value === '~'; + } + + /** + * Should $value cause its operand to eval + * + * @param array $value + * + * @return bool + */ + protected function shouldEval($value) + { + switch ($value[0]) { + case Type::T_EXPRESSION: + if ($value[1] === '/') { + return $this->shouldEval($value[2]) || $this->shouldEval($value[3]); + } + + // fall-thru + case Type::T_VARIABLE: + case Type::T_FUNCTION_CALL: + return true; + } + + return false; + } + + /** + * Reduce value + * + * @param array|Number $value + * @param bool $inExp + * + * @return array|Number + */ + protected function reduce($value, $inExp = false) + { + if ($value instanceof Number) { + return $value; + } + + switch ($value[0]) { + case Type::T_EXPRESSION: + list(, $op, $left, $right, $inParens) = $value; + + $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op; + $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); + + $left = $this->reduce($left, true); + + if ($op !== 'and' && $op !== 'or') { + $right = $this->reduce($right, true); + } + + // special case: looks like css shorthand + if ( + $opName == 'div' && ! $inParens && ! $inExp && + (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') || + ($right[0] === Type::T_NUMBER && ! $right->unitless())) + ) { + return $this->expToString($value); + } + + $left = $this->coerceForExpression($left); + $right = $this->coerceForExpression($right); + $ltype = $left[0]; + $rtype = $right[0]; + + $ucOpName = ucfirst($opName); + $ucLType = ucfirst($ltype); + $ucRType = ucfirst($rtype); + + $shouldEval = $inParens || $inExp; + + // this tries: + // 1. op[op name][left type][right type] + // 2. op[left type][right type] (passing the op as first arg) + // 3. op[op name] + if (\is_callable([$this, $fn = "op{$ucOpName}{$ucLType}{$ucRType}"])) { + $out = $this->$fn($left, $right, $shouldEval); + } elseif (\is_callable([$this, $fn = "op{$ucLType}{$ucRType}"])) { + $out = $this->$fn($op, $left, $right, $shouldEval); + } elseif (\is_callable([$this, $fn = "op{$ucOpName}"])) { + $out = $this->$fn($left, $right, $shouldEval); + } else { + $out = null; + } + + if (isset($out)) { + return $out; + } + + return $this->expToString($value); + + case Type::T_UNARY: + list(, $op, $exp, $inParens) = $value; + + $inExp = $inExp || $this->shouldEval($exp); + $exp = $this->reduce($exp); + + if ($exp instanceof Number) { + switch ($op) { + case '+': + return $exp; + + case '-': + return $exp->unaryMinus(); + } + } + + if ($op === 'not') { + if ($inExp || $inParens) { + if ($exp === static::$false || $exp === static::$null) { + return static::$true; + } + + return static::$false; + } + + $op = $op . ' '; + } + + return [Type::T_STRING, '', [$op, $exp]]; + + case Type::T_VARIABLE: + return $this->reduce($this->get($value[1])); + + case Type::T_LIST: + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + unset($item); + + if (isset($value[3]) && \is_array($value[3])) { + foreach ($value[3] as &$item) { + $item = $this->reduce($item); + } + unset($item); + } + + return $value; + + case Type::T_MAP: + foreach ($value[1] as &$item) { + $item = $this->reduce($item); + } + + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + + return $value; + + case Type::T_STRING: + foreach ($value[2] as &$item) { + if (\is_array($item) || $item instanceof Number) { + $item = $this->reduce($item); + } + } + + return $value; + + case Type::T_INTERPOLATE: + $value[1] = $this->reduce($value[1]); + + if ($inExp) { + return [Type::T_KEYWORD, $this->compileValue($value, false)]; + } + + return $value; + + case Type::T_FUNCTION_CALL: + return $this->fncall($value[1], $value[2]); + + case Type::T_SELF: + $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; + $selfSelector = $this->multiplySelectors($this->env, $selfParent); + $selfSelector = $this->collapseSelectorsAsList($selfSelector); + + return $selfSelector; + + default: + return $value; + } + } + + /** + * Function caller + * + * @param string|array $functionReference + * @param array $argValues + * + * @return array|Number + */ + protected function fncall($functionReference, $argValues) + { + // a string means this is a static hard reference coming from the parsing + if (is_string($functionReference)) { + $name = $functionReference; + + $functionReference = $this->getFunctionReference($name); + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; + } + } + + // a function type means we just want a plain css function call + if ($functionReference[0] === Type::T_FUNCTION) { + // for CSS functions, simply flatten the arguments into a list + $listArgs = []; + + foreach ((array) $argValues as $arg) { + if (empty($arg[0]) || count($argValues) === 1) { + $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1])); + } + } + + return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; + } + + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + return static::$defaultValue; + } + + + switch ($functionReference[1]) { + // SCSS @function + case 'scss': + return $this->callScssFunction($functionReference[3], $argValues); + + // native PHP functions + case 'user': + case 'native': + list(,,$name, $fn, $prototype) = $functionReference; + + // special cases of css valid functions min/max + $name = strtolower($name); + if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) { + $cssFunction = $this->cssValidArg( + [Type::T_FUNCTION_CALL, $name, $argValues], + ['min', 'max', 'calc', 'env', 'var'] + ); + if ($cssFunction !== false) { + return $cssFunction; + } + } + $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues); + + if (! isset($returnValue)) { + return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues); + } + + return $returnValue; + + default: + return static::$defaultValue; + } + } + + /** + * @param array|Number $arg + * @param string[] $allowed_function + * @param bool $inFunction + * + * @return array|Number|false + */ + protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) + { + if ($arg instanceof Number) { + return $this->stringifyFncallArgs($arg); + } + + switch ($arg[0]) { + case Type::T_INTERPOLATE: + return [Type::T_KEYWORD, $this->CompileValue($arg)]; + + case Type::T_FUNCTION: + if (! \in_array($arg[1], $allowed_function)) { + return false; + } + if ($arg[2][0] === Type::T_LIST) { + foreach ($arg[2][2] as $k => $subarg) { + $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]); + if ($arg[2][2][$k] === false) { + return false; + } + } + } + return $arg; + + case Type::T_FUNCTION_CALL: + if (! \in_array($arg[1], $allowed_function)) { + return false; + } + $cssArgs = []; + foreach ($arg[2] as $argValue) { + if ($argValue === static::$null) { + return false; + } + $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]); + if (empty($argValue[0]) && $cssArg !== false) { + $cssArgs[] = [$argValue[0], $cssArg]; + } else { + return false; + } + } + + return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs); + + case Type::T_STRING: + case Type::T_KEYWORD: + if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) { + return false; + } + return $this->stringifyFncallArgs($arg); + + case Type::T_LIST: + if (!$inFunction) { + return false; + } + if (empty($arg['enclosing']) and $arg[1] === '') { + foreach ($arg[2] as $k => $subarg) { + $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction); + if ($arg[2][$k] === false) { + return false; + } + } + $arg[0] = Type::T_STRING; + return $arg; + } + return false; + + case Type::T_EXPRESSION: + if (! \in_array($arg[1], ['+', '-', '/', '*'])) { + return false; + } + $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction); + $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction); + if ($arg[2] === false || $arg[3] === false) { + return false; + } + return $this->expToString($arg, true); + + case Type::T_VARIABLE: + case Type::T_SELF: + default: + return false; + } + } + + + /** + * Reformat fncall arguments to proper css function output + * + * @param array|Number $arg + * + * @return array|Number + */ + protected function stringifyFncallArgs($arg) + { + if ($arg instanceof Number) { + return $arg; + } + + switch ($arg[0]) { + case Type::T_LIST: + foreach ($arg[2] as $k => $v) { + $arg[2][$k] = $this->stringifyFncallArgs($v); + } + break; + + case Type::T_EXPRESSION: + if ($arg[1] === '/') { + $arg[2] = $this->stringifyFncallArgs($arg[2]); + $arg[3] = $this->stringifyFncallArgs($arg[3]); + $arg[5] = $arg[6] = false; // no space around / + $arg = $this->expToString($arg); + } + break; + + case Type::T_FUNCTION_CALL: + $name = strtolower($arg[1]); + + if (in_array($name, ['max', 'min', 'calc'])) { + $args = $arg[2]; + $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args); + } + break; + } + + return $arg; + } + + /** + * Find a function reference + * @param string $name + * @param bool $safeCopy + * @return array + */ + protected function getFunctionReference($name, $safeCopy = false) + { + // SCSS @function + if ($func = $this->get(static::$namespaces['function'] . $name, false)) { + if ($safeCopy) { + $func = clone $func; + } + + return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func]; + } + + // native PHP functions + + // try to find a native lib function + $normalizedName = $this->normalizeName($name); + + if (isset($this->userFunctions[$normalizedName])) { + // see if we can find a user function + list($f, $prototype) = $this->userFunctions[$normalizedName]; + + return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype]; + } + + $lowercasedName = strtolower($normalizedName); + + // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase + // to avoid the deprecation warning about the wrong case being used. + if ($lowercasedName === 'min' || $lowercasedName === 'max' || $lowercasedName === 'rgb' || $lowercasedName === 'rgba' || $lowercasedName === 'hsl' || $lowercasedName === 'hsla') { + $normalizedName = $lowercasedName; + } + + if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { + /** @var string $libName */ + $libName = $f[1]; + $prototype = isset(static::$$libName) ? static::$$libName : null; + + // All core functions have a prototype defined. Not finding the + // prototype can mean 2 things: + // - the function comes from a child class (deprecated just after) + // - the function was found with a different case, which relates to calling the + // wrong Sass function due to our camelCase usage (`fade-in()` vs `fadein()`), + // because PHP method names are case-insensitive while property names are + // case-sensitive. + if ($prototype === null || strtolower($normalizedName) !== $normalizedName) { + $r = new \ReflectionMethod($this, $libName); + $actualLibName = $r->name; + + if ($actualLibName !== $libName || strtolower($normalizedName) !== $normalizedName) { + $kebabCaseName = preg_replace('~(?<=\\w)([A-Z])~', '-$1', substr($actualLibName, 3)); + assert($kebabCaseName !== null); + $originalName = strtolower($kebabCaseName); + $warning = "Calling built-in functions with a non-standard name is deprecated since Scssphp 1.8.0 and will not work anymore in 2.0 (they will be treated as CSS function calls instead).\nUse \"$originalName\" instead of \"$name\"."; + @trigger_error($warning, E_USER_DEPRECATED); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + Warn::deprecation("$warning\n on line $line of $fname"); + + // Use the actual function definition + $prototype = isset(static::$$actualLibName) ? static::$$actualLibName : null; + $f[1] = $libName = $actualLibName; + } + } + + if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) { + $r = new \ReflectionMethod($this, $libName); + $declaringClass = $r->getDeclaringClass()->name; + + $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__; + + if ($needsWarning) { + if (method_exists(__CLASS__, $libName)) { + @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED); + } else { + @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED); + } + } + } + + return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; + } + + return static::$null; + } + + + /** + * Normalize name + * + * @param string $name + * + * @return string + */ + protected function normalizeName($name) + { + return str_replace('-', '_', $name); + } + + /** + * Normalize value + * + * @internal + * + * @param array|Number $value + * + * @return array|Number + */ + public function normalizeValue($value) + { + $value = $this->coerceForExpression($this->reduce($value)); + + if ($value instanceof Number) { + return $value; + } + + switch ($value[0]) { + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return [Type::T_KEYWORD, $this->compileValue($value)]; + } + + foreach ($value[2] as $key => $item) { + $value[2][$key] = $this->normalizeValue($item); + } + + if (! empty($value['enclosing'])) { + unset($value['enclosing']); + } + + if ($value[1] === '' && count($value[2]) > 1) { + $value[1] = ' '; + } + + return $value; + + case Type::T_STRING: + return [$value[0], '"', [$this->compileStringContent($value)]]; + + case Type::T_INTERPOLATE: + return [Type::T_KEYWORD, $this->compileValue($value)]; + + default: + return $value; + } + } + + /** + * Add numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opAddNumberNumber(Number $left, Number $right) + { + return $left->plus($right); + } + + /** + * Multiply numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opMulNumberNumber(Number $left, Number $right) + { + return $left->times($right); + } + + /** + * Subtract numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opSubNumberNumber(Number $left, Number $right) + { + return $left->minus($right); + } + + /** + * Divide numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opDivNumberNumber(Number $left, Number $right) + { + return $left->dividedBy($right); + } + + /** + * Mod numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opModNumberNumber(Number $left, Number $right) + { + return $left->modulo($right); + } + + /** + * Add strings + * + * @param array $left + * @param array $right + * + * @return array|null + */ + protected function opAdd($left, $right) + { + if ($strLeft = $this->coerceString($left)) { + if ($right[0] === Type::T_STRING) { + $right[1] = ''; + } + + $strLeft[2][] = $right; + + return $strLeft; + } + + if ($strRight = $this->coerceString($right)) { + if ($left[0] === Type::T_STRING) { + $left[1] = ''; + } + + array_unshift($strRight[2], $left); + + return $strRight; + } + + return null; + } + + /** + * Boolean and + * + * @param array|Number $left + * @param array|Number $right + * @param bool $shouldEval + * + * @return array|Number|null + */ + protected function opAnd($left, $right, $shouldEval) + { + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); + + if (! $shouldEval) { + if (! $truthy) { + return null; + } + } + + if ($left !== static::$false && $left !== static::$null) { + return $this->reduce($right, true); + } + + return $left; + } + + /** + * Boolean or + * + * @param array|Number $left + * @param array|Number $right + * @param bool $shouldEval + * + * @return array|Number|null + */ + protected function opOr($left, $right, $shouldEval) + { + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); + + if (! $shouldEval) { + if (! $truthy) { + return null; + } + } + + if ($left !== static::$false && $left !== static::$null) { + return $left; + } + + return $this->reduce($right, true); + } + + /** + * Compare colors + * + * @param string $op + * @param array $left + * @param array $right + * + * @return array + */ + protected function opColorColor($op, $left, $right) + { + if ($op !== '==' && $op !== '!=') { + $warning = "Color arithmetic is deprecated and will be an error in future versions.\n" + . "Consider using Sass's color functions instead."; + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + Warn::deprecation("$warning\n on line $line of $fname"); + } + + $out = [Type::T_COLOR]; + + foreach ([1, 2, 3] as $i) { + $lval = isset($left[$i]) ? $left[$i] : 0; + $rval = isset($right[$i]) ? $right[$i] : 0; + + switch ($op) { + case '+': + $out[] = $lval + $rval; + break; + + case '-': + $out[] = $lval - $rval; + break; + + case '*': + $out[] = $lval * $rval; + break; + + case '%': + if ($rval == 0) { + throw $this->error("color: Can't take modulo by zero"); + } + + $out[] = $lval % $rval; + break; + + case '/': + if ($rval == 0) { + throw $this->error("color: Can't divide by zero"); + } + + $out[] = (int) ($lval / $rval); + break; + + case '==': + return $this->opEq($left, $right); + + case '!=': + return $this->opNeq($left, $right); + + default: + throw $this->error("color: unknown op $op"); + } + } + + if (isset($left[4])) { + $out[4] = $left[4]; + } elseif (isset($right[4])) { + $out[4] = $right[4]; + } + + return $this->fixColor($out); + } + + /** + * Compare color and number + * + * @param string $op + * @param array $left + * @param Number $right + * + * @return array + */ + protected function opColorNumber($op, $left, Number $right) + { + if ($op === '==') { + return static::$false; + } + + if ($op === '!=') { + return static::$true; + } + + $value = $right->getDimension(); + + return $this->opColorColor( + $op, + $left, + [Type::T_COLOR, $value, $value, $value] + ); + } + + /** + * Compare number and color + * + * @param string $op + * @param Number $left + * @param array $right + * + * @return array + */ + protected function opNumberColor($op, Number $left, $right) + { + if ($op === '==') { + return static::$false; + } + + if ($op === '!=') { + return static::$true; + } + + $value = $left->getDimension(); + + return $this->opColorColor( + $op, + [Type::T_COLOR, $value, $value, $value], + $right + ); + } + + /** + * Compare number1 == number2 + * + * @param array|Number $left + * @param array|Number $right + * + * @return array + */ + protected function opEq($left, $right) + { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ''; + $rStr[1] = ''; + + $left = $this->compileValue($lStr); + $right = $this->compileValue($rStr); + } + + return $this->toBool($left === $right); + } + + /** + * Compare number1 != number2 + * + * @param array|Number $left + * @param array|Number $right + * + * @return array + */ + protected function opNeq($left, $right) + { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ''; + $rStr[1] = ''; + + $left = $this->compileValue($lStr); + $right = $this->compileValue($rStr); + } + + return $this->toBool($left !== $right); + } + + /** + * Compare number1 == number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opEqNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->equals($right)); + } + + /** + * Compare number1 != number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opNeqNumberNumber(Number $left, Number $right) + { + return $this->toBool(!$left->equals($right)); + } + + /** + * Compare number1 >= number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opGteNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->greaterThanOrEqual($right)); + } + + /** + * Compare number1 > number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opGtNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->greaterThan($right)); + } + + /** + * Compare number1 <= number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opLteNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->lessThanOrEqual($right)); + } + + /** + * Compare number1 < number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opLtNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->lessThan($right)); + } + + /** + * Cast to boolean + * + * @api + * + * @param bool $thing + * + * @return array + */ + public function toBool($thing) + { + return $thing ? static::$true : static::$false; + } + + /** + * Escape non printable chars in strings output as in dart-sass + * + * @internal + * + * @param string $string + * @param bool $inKeyword + * + * @return string + */ + public function escapeNonPrintableChars($string, $inKeyword = false) + { + static $replacement = []; + if (empty($replacement[$inKeyword])) { + for ($i = 0; $i < 32; $i++) { + if ($i !== 9 || $inKeyword) { + $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0)); + } + } + } + $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string); + // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement + if (strpos($string, chr(0)) !== false) { + if (substr($string, -1) === chr(0)) { + $string = substr($string, 0, -1); + } + $string = str_replace( + [chr(0) . '\\',chr(0) . ' '], + [ '\\', ' '], + $string + ); + if (strpos($string, chr(0)) !== false) { + $parts = explode(chr(0), $string); + $string = array_shift($parts); + while (count($parts)) { + $next = array_shift($parts); + if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) { + $string .= " "; + } + $string .= $next; + } + } + } + + return $string; + } + + /** + * Compiles a primitive value into a CSS property value. + * + * Values in scssphp are typed by being wrapped in arrays, their format is + * typically: + * + * array(type, contents [, additional_contents]*) + * + * The input is expected to be reduced. This function will not work on + * things like expressions and variables. + * + * @api + * + * @param array|Number $value + * @param bool $quote + * + * @return string + */ + public function compileValue($value, $quote = true) + { + $value = $this->reduce($value); + + if ($value instanceof Number) { + return $value->output($this); + } + + switch ($value[0]) { + case Type::T_KEYWORD: + return $this->escapeNonPrintableChars($value[1], true); + + case Type::T_COLOR: + // [1] - red component (either number for a %) + // [2] - green component + // [3] - blue component + // [4] - optional alpha component + list(, $r, $g, $b) = $value; + + $r = $this->compileRGBAValue($r); + $g = $this->compileRGBAValue($g); + $b = $this->compileRGBAValue($b); + + if (\count($value) === 5) { + $alpha = $this->compileRGBAValue($value[4], true); + + if (! is_numeric($alpha) || $alpha < 1) { + $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha); + + if (! \is_null($colorName)) { + return $colorName; + } + + if (is_numeric($alpha)) { + $a = new Number($alpha, ''); + } else { + $a = $alpha; + } + + return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')'; + } + } + + if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) { + return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')'; + } + + $colorName = Colors::RGBaToColorName($r, $g, $b); + + if (! \is_null($colorName)) { + return $colorName; + } + + $h = sprintf('#%02x%02x%02x', $r, $g, $b); + + // Converting hex color to short notation (e.g. #003399 to #039) + if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { + $h = '#' . $h[1] . $h[3] . $h[5]; + } + + return $h; + + case Type::T_STRING: + $content = $this->compileStringContent($value, $quote); + + if ($value[1] && $quote) { + $content = str_replace('\\', '\\\\', $content); + + $content = $this->escapeNonPrintableChars($content); + + // force double quote as string quote for the output in certain cases + if ( + $value[1] === "'" && + (strpos($content, '"') === false or strpos($content, "'") !== false) + ) { + $value[1] = '"'; + } elseif ( + $value[1] === '"' && + (strpos($content, '"') !== false and strpos($content, "'") === false) + ) { + $value[1] = "'"; + } + + $content = str_replace($value[1], '\\' . $value[1], $content); + } + + return $value[1] . $content . $value[1]; + + case Type::T_FUNCTION: + $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : ''; + + return "$value[1]($args)"; + + case Type::T_FUNCTION_REFERENCE: + $name = ! empty($value[2]) ? $value[2] : ''; + + return "get-function(\"$name\")"; + + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return $this->compileValue($value, $quote); + } + + list(, $delim, $items) = $value; + $pre = $post = ''; + + if (! empty($value['enclosing'])) { + switch ($value['enclosing']) { + case 'parent': + //$pre = '('; + //$post = ')'; + break; + case 'forced_parent': + $pre = '('; + $post = ')'; + break; + case 'bracket': + case 'forced_bracket': + $pre = '['; + $post = ']'; + break; + } + } + + $separator = $delim === '/' ? ' /' : $delim; + + $prefix_value = ''; + + if ($delim !== ' ') { + $prefix_value = ' '; + } + + $filtered = []; + + $same_string_quote = null; + foreach ($items as $item) { + if (\is_null($same_string_quote)) { + $same_string_quote = false; + if ($item[0] === Type::T_STRING) { + $same_string_quote = $item[1]; + foreach ($items as $ii) { + if ($ii[0] !== Type::T_STRING) { + $same_string_quote = false; + break; + } + } + } + } + if ($item[0] === Type::T_NULL) { + continue; + } + if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) { + $item[1] = $same_string_quote; + } + + $compiled = $this->compileValue($item, $quote); + + if ($prefix_value && \strlen($compiled)) { + $compiled = $prefix_value . $compiled; + } + + $filtered[] = $compiled; + } + + return $pre . substr(implode($separator, $filtered), \strlen($prefix_value)) . $post; + + case Type::T_MAP: + $keys = $value[1]; + $values = $value[2]; + $filtered = []; + + for ($i = 0, $s = \count($keys); $i < $s; $i++) { + $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote); + } + + array_walk($filtered, function (&$value, $key) { + $value = $key . ': ' . $value; + }); + + return '(' . implode(', ', $filtered) . ')'; + + case Type::T_INTERPOLATED: + // node created by extractInterpolation + list(, $interpolate, $left, $right) = $value; + list(,, $whiteLeft, $whiteRight) = $interpolate; + + $delim = $left[1]; + + if ($delim && $delim !== ' ' && ! $whiteLeft) { + $delim .= ' '; + } + + $left = \count($left[2]) > 0 + ? $this->compileValue($left, $quote) . $delim . $whiteLeft + : ''; + + $delim = $right[1]; + + if ($delim && $delim !== ' ') { + $delim .= ' '; + } + + $right = \count($right[2]) > 0 ? + $whiteRight . $delim . $this->compileValue($right, $quote) : ''; + + return $left . $this->compileValue($interpolate, $quote) . $right; + + case Type::T_INTERPOLATE: + // strip quotes if it's a string + $reduced = $this->reduce($value[1]); + + if ($reduced instanceof Number) { + return $this->compileValue($reduced, $quote); + } + + switch ($reduced[0]) { + case Type::T_LIST: + $reduced = $this->extractInterpolation($reduced); + + if ($reduced[0] !== Type::T_LIST) { + break; + } + + list(, $delim, $items) = $reduced; + + if ($delim !== ' ') { + $delim .= ' '; + } + + $filtered = []; + + foreach ($items as $item) { + if ($item[0] === Type::T_NULL) { + continue; + } + + if ($item[0] === Type::T_STRING) { + $filtered[] = $this->compileStringContent($item, $quote); + } elseif ($item[0] === Type::T_KEYWORD) { + $filtered[] = $item[1]; + } else { + $filtered[] = $this->compileValue($item, $quote); + } + } + + $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)]; + break; + + case Type::T_STRING: + $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]]; + break; + + case Type::T_NULL: + $reduced = [Type::T_KEYWORD, '']; + } + + return $this->compileValue($reduced, $quote); + + case Type::T_NULL: + return 'null'; + + case Type::T_COMMENT: + return $this->compileCommentValue($value); + + default: + throw $this->error('unknown value type: ' . json_encode($value)); + } + } + + /** + * @param array|Number $value + * + * @return string + */ + protected function compileDebugValue($value) + { + $value = $this->reduce($value, true); + + if ($value instanceof Number) { + return $this->compileValue($value); + } + + switch ($value[0]) { + case Type::T_STRING: + return $this->compileStringContent($value); + + default: + return $this->compileValue($value); + } + } + + /** + * Flatten list + * + * @param array $list + * + * @return string + * + * @deprecated + */ + protected function flattenList($list) + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + return $this->compileValue($list); + } + + /** + * Gets the text of a Sass string + * + * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first + * to ensure that the value is indeed a string. + * + * @param array $value + * + * @return string + */ + public function getStringText(array $value) + { + if ($value[0] !== Type::T_STRING) { + throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?'); + } + + return $this->compileStringContent($value); + } + + /** + * Compile string content + * + * @param array $string + * @param bool $quote + * + * @return string + */ + protected function compileStringContent($string, $quote = true) + { + $parts = []; + + foreach ($string[2] as $part) { + if (\is_array($part) || $part instanceof Number) { + $parts[] = $this->compileValue($part, $quote); + } else { + $parts[] = $part; + } + } + + return implode($parts); + } + + /** + * Extract interpolation; it doesn't need to be recursive, compileValue will handle that + * + * @param array $list + * + * @return array + */ + protected function extractInterpolation($list) + { + $items = $list[2]; + + foreach ($items as $i => $item) { + if ($item[0] === Type::T_INTERPOLATE) { + $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)]; + $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)]; + + return [Type::T_INTERPOLATED, $item, $before, $after]; + } + } + + return $list; + } + + /** + * Find the final set of selectors + * + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param \ScssPhp\ScssPhp\Block $selfParent + * + * @return array + */ + protected function multiplySelectors(Environment $env, $selfParent = null) + { + $envs = $this->compactEnv($env); + $selectors = []; + $parentSelectors = [[]]; + + $selfParentSelectors = null; + + if (! \is_null($selfParent) && $selfParent->selectors) { + $selfParentSelectors = $this->evalSelectors($selfParent->selectors); + } + + while ($env = array_pop($envs)) { + if (empty($env->selectors)) { + continue; + } + + $selectors = $env->selectors; + + do { + $stillHasSelf = false; + $prevSelectors = $selectors; + $selectors = []; + + foreach ($parentSelectors as $parent) { + foreach ($prevSelectors as $selector) { + if ($selfParentSelectors) { + foreach ($selfParentSelectors as $selfParent) { + // if no '&' in the selector, each call will give same result, only add once + $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent); + $selectors[serialize($s)] = $s; + } + } else { + $s = $this->joinSelectors($parent, $selector, $stillHasSelf); + $selectors[serialize($s)] = $s; + } + } + } + } while ($stillHasSelf); + + $parentSelectors = $selectors; + } + + $selectors = array_values($selectors); + + // case we are just starting a at-root : nothing to multiply but parentSelectors + if (! $selectors && $selfParentSelectors) { + $selectors = $selfParentSelectors; + } + + return $selectors; + } + + /** + * Join selectors; looks for & to replace, or append parent before child + * + * @param array $parent + * @param array $child + * @param bool $stillHasSelf + * @param array $selfParentSelectors + + * @return array + */ + protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null) + { + $setSelf = false; + $out = []; + + foreach ($child as $part) { + $newPart = []; + + foreach ($part as $p) { + // only replace & once and should be recalled to be able to make combinations + if ($p === static::$selfSelector && $setSelf) { + $stillHasSelf = true; + } + + if ($p === static::$selfSelector && ! $setSelf) { + $setSelf = true; + + if (\is_null($selfParentSelectors)) { + $selfParentSelectors = $parent; + } + + foreach ($selfParentSelectors as $i => $parentPart) { + if ($i > 0) { + $out[] = $newPart; + $newPart = []; + } + + foreach ($parentPart as $pp) { + if (\is_array($pp)) { + $flatten = []; + + array_walk_recursive($pp, function ($a) use (&$flatten) { + $flatten[] = $a; + }); + + $pp = implode($flatten); + } + + $newPart[] = $pp; + } + } + } else { + $newPart[] = $p; + } + } + + $out[] = $newPart; + } + + return $setSelf ? $out : array_merge($parent, $child); + } + + /** + * Multiply media + * + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param array $childQueries + * + * @return array + */ + protected function multiplyMedia(Environment $env = null, $childQueries = null) + { + if ( + ! isset($env) || + ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA + ) { + return $childQueries; + } + + // plain old block, skip + if (empty($env->block->type)) { + return $this->multiplyMedia($env->parent, $childQueries); + } + + assert($env->block instanceof MediaBlock); + + $parentQueries = isset($env->block->queryList) + ? $env->block->queryList + : [[[Type::T_MEDIA_VALUE, $env->block->value]]]; + + $store = [$this->env, $this->storeEnv]; + + $this->env = $env; + $this->storeEnv = null; + $parentQueries = $this->evaluateMediaQuery($parentQueries); + + list($this->env, $this->storeEnv) = $store; + + if (\is_null($childQueries)) { + $childQueries = $parentQueries; + } else { + $originalQueries = $childQueries; + $childQueries = []; + + foreach ($parentQueries as $parentQuery) { + foreach ($originalQueries as $childQuery) { + $childQueries[] = array_merge( + $parentQuery, + [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]], + $childQuery + ); + } + } + } + + return $this->multiplyMedia($env->parent, $childQueries); + } + + /** + * Convert env linked list to stack + * + * @param Environment $env + * + * @return Environment[] + * + * @phpstan-return non-empty-array + */ + protected function compactEnv(Environment $env) + { + for ($envs = []; $env; $env = $env->parent) { + $envs[] = $env; + } + + return $envs; + } + + /** + * Convert env stack to singly linked list + * + * @param Environment[] $envs + * + * @return Environment + * + * @phpstan-param non-empty-array $envs + */ + protected function extractEnv($envs) + { + for ($env = null; $e = array_pop($envs);) { + $e->parent = $env; + $env = $e; + } + + return $env; + } + + /** + * Push environment + * + * @param \ScssPhp\ScssPhp\Block $block + * + * @return \ScssPhp\ScssPhp\Compiler\Environment + */ + protected function pushEnv(Block $block = null) + { + $env = new Environment(); + $env->parent = $this->env; + $env->parentStore = $this->storeEnv; + $env->store = []; + $env->block = $block; + $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; + + $this->env = $env; + $this->storeEnv = null; + + return $env; + } + + /** + * Pop environment + * + * @return void + */ + protected function popEnv() + { + $this->storeEnv = $this->env->parentStore; + $this->env = $this->env->parent; + } + + /** + * Propagate vars from a just poped Env (used in @each and @for) + * + * @param array $store + * @param null|string[] $excludedVars + * + * @return void + */ + protected function backPropagateEnv($store, $excludedVars = null) + { + foreach ($store as $key => $value) { + if (empty($excludedVars) || ! \in_array($key, $excludedVars)) { + $this->set($key, $value, true); + } + } + } + + /** + * Get store environment + * + * @return \ScssPhp\ScssPhp\Compiler\Environment + */ + protected function getStoreEnv() + { + return isset($this->storeEnv) ? $this->storeEnv : $this->env; + } + + /** + * Set variable + * + * @param string $name + * @param mixed $value + * @param bool $shadow + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced + * + * @return void + */ + protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null) + { + $name = $this->normalizeName($name); + + if (! isset($env)) { + $env = $this->getStoreEnv(); + } + + if ($shadow) { + $this->setRaw($name, $value, $env, $valueUnreduced); + } else { + $this->setExisting($name, $value, $env, $valueUnreduced); + } + } + + /** + * Set existing variable + * + * @param string $name + * @param mixed $value + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced + * + * @return void + */ + protected function setExisting($name, $value, Environment $env, $valueUnreduced = null) + { + $storeEnv = $env; + $specialContentKey = static::$namespaces['special'] . 'content'; + + $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; + + $maxDepth = 10000; + + for (;;) { + if ($maxDepth-- <= 0) { + break; + } + + if (\array_key_exists($name, $env->store)) { + break; + } + + if (! $hasNamespace && isset($env->marker)) { + if (! empty($env->store[$specialContentKey])) { + $env = $env->store[$specialContentKey]->scope; + continue; + } + + if (! empty($env->declarationScopeParent)) { + $env = $env->declarationScopeParent; + continue; + } else { + $env = $storeEnv; + break; + } + } + + if (isset($env->parentStore)) { + $env = $env->parentStore; + } elseif (isset($env->parent)) { + $env = $env->parent; + } else { + $env = $storeEnv; + break; + } + } + + $env->store[$name] = $value; + + if ($valueUnreduced) { + $env->storeUnreduced[$name] = $valueUnreduced; + } + } + + /** + * Set raw variable + * + * @param string $name + * @param mixed $value + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced + * + * @return void + */ + protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) + { + $env->store[$name] = $value; + + if ($valueUnreduced) { + $env->storeUnreduced[$name] = $valueUnreduced; + } + } + + /** + * Get variable + * + * @internal + * + * @param string $name + * @param bool $shouldThrow + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param bool $unreduced + * + * @return mixed|null + */ + public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false) + { + $normalizedName = $this->normalizeName($name); + $specialContentKey = static::$namespaces['special'] . 'content'; + + if (! isset($env)) { + $env = $this->getStoreEnv(); + } + + $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%'; + + $maxDepth = 10000; + + for (;;) { + if ($maxDepth-- <= 0) { + break; + } + + if (\array_key_exists($normalizedName, $env->store)) { + if ($unreduced && isset($env->storeUnreduced[$normalizedName])) { + return $env->storeUnreduced[$normalizedName]; + } + + return $env->store[$normalizedName]; + } + + if (! $hasNamespace && isset($env->marker)) { + if (! empty($env->store[$specialContentKey])) { + $env = $env->store[$specialContentKey]->scope; + continue; + } + + if (! empty($env->declarationScopeParent)) { + $env = $env->declarationScopeParent; + } else { + $env = $this->rootEnv; + } + continue; + } + + if (isset($env->parentStore)) { + $env = $env->parentStore; + } elseif (isset($env->parent)) { + $env = $env->parent; + } else { + break; + } + } + + if ($shouldThrow) { + throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : '')); + } + + // found nothing + return null; + } + + /** + * Has variable? + * + * @param string $name + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * + * @return bool + */ + protected function has($name, Environment $env = null) + { + return ! \is_null($this->get($name, false, $env)); + } + + /** + * Inject variables + * + * @param array $args + * + * @return void + */ + protected function injectVariables(array $args) + { + if (empty($args)) { + return; + } + + $parser = $this->parserFactory(__METHOD__); + + foreach ($args as $name => $strValue) { + if ($name[0] === '$') { + $name = substr($name, 1); + } + + if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) { + $value = $this->coerceValue($strValue); + } + + $this->set($name, $value); + } + } + + /** + * Replaces variables. + * + * @param array $variables + * + * @return void + */ + public function replaceVariables(array $variables) + { + $this->registeredVars = []; + $this->addVariables($variables); + } + + /** + * Replaces variables. + * + * @param array $variables + * + * @return void + */ + public function addVariables(array $variables) + { + $triggerWarning = false; + + foreach ($variables as $name => $value) { + if (!$value instanceof Number && !\is_array($value)) { + $triggerWarning = true; + } + + $this->registeredVars[$name] = $value; + } + + if ($triggerWarning) { + @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED); + } + } + + /** + * Set variables + * + * @api + * + * @param array $variables + * + * @return void + * + * @deprecated Use "addVariables" or "replaceVariables" instead. + */ + public function setVariables(array $variables) + { + @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.'); + + $this->addVariables($variables); + } + + /** + * Unset variable + * + * @api + * + * @param string $name + * + * @return void + */ + public function unsetVariable($name) + { + unset($this->registeredVars[$name]); + } + + /** + * Returns list of variables + * + * @api + * + * @return array + */ + public function getVariables() + { + return $this->registeredVars; + } + + /** + * Adds to list of parsed files + * + * @internal + * + * @param string|null $path + * + * @return void + */ + public function addParsedFile($path) + { + if (! \is_null($path) && is_file($path)) { + $this->parsedFiles[realpath($path)] = filemtime($path); + } + } + + /** + * Returns list of parsed files + * + * @deprecated + * @return array + */ + public function getParsedFiles() + { + @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED); + return $this->parsedFiles; + } + + /** + * Add import path + * + * @api + * + * @param string|callable $path + * + * @return void + */ + public function addImportPath($path) + { + if (! \in_array($path, $this->importPaths)) { + $this->importPaths[] = $path; + } + } + + /** + * Set import paths + * + * @api + * + * @param string|array $path + * + * @return void + */ + public function setImportPaths($path) + { + $paths = (array) $path; + $actualImportPaths = array_filter($paths, function ($path) { + return $path !== ''; + }); + + $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths); + + if ($this->legacyCwdImportPath) { + @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); + } + + $this->importPaths = $actualImportPaths; + } + + /** + * Set number precision + * + * @api + * + * @param int $numberPrecision + * + * @return void + * + * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. + */ + public function setNumberPrecision($numberPrecision) + { + @trigger_error('The number precision is not configurable anymore. ' + . 'The default is enough for all browsers.', E_USER_DEPRECATED); + } + + /** + * Sets the output style. + * + * @api + * + * @param string $style One of the OutputStyle constants + * + * @return void + * + * @phpstan-param OutputStyle::* $style + */ + public function setOutputStyle($style) + { + switch ($style) { + case OutputStyle::EXPANDED: + $this->configuredFormatter = Expanded::class; + break; + + case OutputStyle::COMPRESSED: + $this->configuredFormatter = Compressed::class; + break; + + default: + throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style)); + } + } + + /** + * Set formatter + * + * @api + * + * @param string $formatterName + * + * @return void + * + * @deprecated Use {@see setOutputStyle} instead. + * + * @phpstan-param class-string $formatterName + */ + public function setFormatter($formatterName) + { + if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) { + @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED); + } + @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED); + + $this->configuredFormatter = $formatterName; + } + + /** + * Set line number style + * + * @api + * + * @param string $lineNumberStyle + * + * @return void + * + * @deprecated The line number output is not supported anymore. Use source maps instead. + */ + public function setLineNumberStyle($lineNumberStyle) + { + @trigger_error('The line number output is not supported anymore. ' + . 'Use source maps instead.', E_USER_DEPRECATED); + } + + /** + * Configures the handling of non-ASCII outputs. + * + * If $charset is `true`, this will include a `@charset` declaration or a + * UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII + * characters. Otherwise, it will never include a `@charset` declaration or a + * byte-order mark. + * + * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 + * + * @param bool $charset + * + * @return void + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Enable/disable source maps + * + * @api + * + * @param int $sourceMap + * + * @return void + * + * @phpstan-param self::SOURCE_MAP_* $sourceMap + */ + public function setSourceMap($sourceMap) + { + $this->sourceMap = $sourceMap; + } + + /** + * Set source map options + * + * @api + * + * @param array $sourceMapOptions + * + * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions + * + * @return void + */ + public function setSourceMapOptions($sourceMapOptions) + { + $this->sourceMapOptions = $sourceMapOptions; + } + + /** + * Register function + * + * @api + * + * @param string $name + * @param callable $callback + * @param string[]|null $argumentDeclaration + * + * @return void + */ + public function registerFunction($name, $callback, $argumentDeclaration = null) + { + if (self::isNativeFunction($name)) { + @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED); + } + + if ($argumentDeclaration === null) { + @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED); + } + + $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration]; + } + + /** + * Unregister function + * + * @api + * + * @param string $name + * + * @return void + */ + public function unregisterFunction($name) + { + unset($this->userFunctions[$this->normalizeName($name)]); + } + + /** + * Add feature + * + * @api + * + * @param string $name + * + * @return void + * + * @deprecated Registering additional features is deprecated. + */ + public function addFeature($name) + { + @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED); + + $this->registeredFeatures[$name] = true; + } + + /** + * Import file + * + * @param string $path + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void + */ + protected function importFile($path, OutputBlock $out) + { + $this->pushCallStack('import ' . $this->getPrettyPath($path)); + // see if tree is cached + $realPath = realpath($path); + + if ($realPath === false) { + $realPath = $path; + } + + if (substr($path, -5) === '.sass') { + $this->sourceIndex = \count($this->sourceNames); + $this->sourceNames[] = $path; + $this->sourceLine = 1; + $this->sourceColumn = 1; + + throw $this->error('The Sass indented syntax is not implemented.'); + } + + if (isset($this->importCache[$realPath])) { + $this->handleImportLoop($realPath); + + $tree = $this->importCache[$realPath]; + } else { + $code = file_get_contents($path); + $parser = $this->parserFactory($path); + $tree = $parser->parse($code); + + $this->importCache[$realPath] = $tree; + } + + $currentDirectory = $this->currentDirectory; + $this->currentDirectory = dirname($path); + + $this->compileChildrenNoReturn($tree->children, $out); + $this->currentDirectory = $currentDirectory; + $this->popCallStack(); + } + + /** + * Save the imported files with their resolving path context + * + * @param string|null $currentDirectory + * @param string $path + * @param string $filePath + * + * @return void + */ + private function registerImport($currentDirectory, $path, $filePath) + { + $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath]; + } + + /** + * Detects whether the import is a CSS import. + * + * For legacy reasons, custom importers are called for those, allowing them + * to replace them with an actual Sass import. However this behavior is + * deprecated. Custom importers are expected to return null when they receive + * a CSS import. + * + * @param string $url + * + * @return bool + */ + public static function isCssImport($url) + { + return 1 === preg_match('~\.css$|^https?://|^//~', $url); + } + + /** + * Return the file path for an import url if it exists + * + * @internal + * + * @param string $url + * @param string|null $currentDir + * + * @return string|null + */ + public function findImport($url, $currentDir = null) + { + // Vanilla css and external requests. These are not meant to be Sass imports. + // Callback importers are still called for BC. + if (self::isCssImport($url)) { + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + continue; + } + + if (\is_callable($dir)) { + // check custom callback for import path + $file = \call_user_func($dir, $url); + + if (! \is_null($file)) { + if (\is_array($dir)) { + $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1]; + } elseif ($dir instanceof \Closure) { + $r = new \ReflectionFunction($dir); + if (false !== strpos($r->name, '{closure}')) { + $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine()); + } elseif ($class = $r->getClosureScopeClass()) { + $callableDescription = $class->name.'::'.$r->name; + } else { + $callableDescription = $r->name; + } + } elseif (\is_object($dir)) { + $callableDescription = \get_class($dir) . '::__invoke'; + } else { + $callableDescription = 'callable'; // Fallback if we don't have a dedicated description + } + @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED); + + return $file; + } + } + } + return null; + } + + if (!\is_null($currentDir)) { + $relativePath = $this->resolveImportPath($url, $currentDir); + + if (!\is_null($relativePath)) { + return $relativePath; + } + } + + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + $path = $this->resolveImportPath($url, $dir); + + if (!\is_null($path)) { + return $path; + } + } elseif (\is_callable($dir)) { + // check custom callback for import path + $file = \call_user_func($dir, $url); + + if (! \is_null($file)) { + return $file; + } + } + } + + if ($this->legacyCwdImportPath) { + $path = $this->resolveImportPath($url, getcwd()); + + if (!\is_null($path)) { + @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); + + return $path; + } + } + + throw $this->error("`$url` file not found for @import"); + } + + /** + * @param string $url + * @param string $baseDir + * + * @return string|null + */ + private function resolveImportPath($url, $baseDir) + { + $path = Path::join($baseDir, $url); + + $hasExtension = preg_match('/.s[ac]ss$/', $url); + + if ($hasExtension) { + return $this->checkImportPathConflicts($this->tryImportPath($path)); + } + + $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path)); + + if (!\is_null($result)) { + return $result; + } + + return $this->tryImportPathAsDirectory($path); + } + + /** + * @param string[] $paths + * + * @return string|null + */ + private function checkImportPathConflicts(array $paths) + { + if (\count($paths) === 0) { + return null; + } + + if (\count($paths) === 1) { + return $paths[0]; + } + + $formattedPrettyPaths = []; + + foreach ($paths as $path) { + $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path); + } + + throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths)); + } + + /** + * @param string $path + * + * @return string[] + */ + private function tryImportPathWithExtensions($path) + { + $result = array_merge( + $this->tryImportPath($path.'.sass'), + $this->tryImportPath($path.'.scss') + ); + + if ($result) { + return $result; + } + + return $this->tryImportPath($path.'.css'); + } + + /** + * @param string $path + * + * @return string[] + */ + private function tryImportPath($path) + { + $partial = dirname($path).'/_'.basename($path); + + $candidates = []; + + if (is_file($partial)) { + $candidates[] = $partial; + } + + if (is_file($path)) { + $candidates[] = $path; + } + + return $candidates; + } + + /** + * @param string $path + * + * @return string|null + */ + private function tryImportPathAsDirectory($path) + { + if (!is_dir($path)) { + return null; + } + + return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index')); + } + + /** + * @param string|null $path + * + * @return string + */ + private function getPrettyPath($path) + { + if ($path === null) { + return '(unknown file)'; + } + + $normalizedPath = $path; + $normalizedRootDirectory = $this->rootDirectory.'/'; + + if (\DIRECTORY_SEPARATOR === '\\') { + $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); + $normalizedPath = str_replace('\\', '/', $path); + } + + if (0 === strpos($normalizedPath, $normalizedRootDirectory)) { + return substr($path, \strlen($normalizedRootDirectory)); + } + + return $path; + } + + /** + * Set encoding + * + * @api + * + * @param string|null $encoding + * + * @return void + * + * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated. + */ + public function setEncoding($encoding) + { + if (!$encoding || strtolower($encoding) === 'utf-8') { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + } else { + @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED); + } + + $this->encoding = $encoding; + } + + /** + * Ignore errors? + * + * @api + * + * @param bool $ignoreErrors + * + * @return \ScssPhp\ScssPhp\Compiler + * + * @deprecated Ignoring Sass errors is not longer supported. + */ + public function setIgnoreErrors($ignoreErrors) + { + @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); + + return $this; + } + + /** + * Get source position + * + * @api + * + * @return array + * + * @deprecated + */ + public function getSourcePosition() + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; + + return [$sourceFile, $this->sourceLine, $this->sourceColumn]; + } + + /** + * Throw error (exception) + * + * @api + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * + * @return never + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException + * + * @deprecated use "error" and throw the exception in the caller instead. + */ + public function throwError($msg) + { + @trigger_error( + 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->error(...func_get_args()); + } + + /** + * Build an error (exception) + * + * @internal + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * @param bool|float|int|string|null ...$args + * + * @return CompilerException + */ + public function error($msg, ...$args) + { + if ($args) { + $msg = sprintf($msg, ...$args); + } + + if (! $this->ignoreCallStackMessage) { + $msg = $this->addLocationToMessage($msg); + } + + return new CompilerException($msg); + } + + /** + * @param string $msg + * + * @return string + */ + private function addLocationToMessage($msg) + { + $line = $this->sourceLine; + $column = $this->sourceColumn; + + $loc = isset($this->sourceNames[$this->sourceIndex]) + ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" + : "line: $line, column: $column"; + + $msg = "$msg: $loc"; + + $callStackMsg = $this->callStackMessage(); + + if ($callStackMsg) { + $msg .= "\nCall Stack:\n" . $callStackMsg; + } + + return $msg; + } + + /** + * @param string $functionName + * @param array $ExpectedArgs + * @param int $nbActual + * @return CompilerException + * + * @deprecated + */ + public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + $nbExpected = \count($ExpectedArgs); + + if ($nbActual > $nbExpected) { + return $this->error( + 'Error: Only %d arguments allowed in %s(), but %d were passed.', + $nbExpected, + $functionName, + $nbActual + ); + } else { + $missing = []; + + while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) { + array_unshift($missing, array_pop($ExpectedArgs)); + } + + return $this->error( + 'Error: %s() argument%s %s missing.', + $functionName, + count($missing) > 1 ? 's' : '', + implode(', ', $missing) + ); + } + } + + /** + * Beautify call stack for output + * + * @param bool $all + * @param int|null $limit + * + * @return string + */ + protected function callStackMessage($all = false, $limit = null) + { + $callStackMsg = []; + $ncall = 0; + + if ($this->callStack) { + foreach (array_reverse($this->callStack) as $call) { + if ($all || (isset($call['n']) && $call['n'])) { + $msg = '#' . $ncall++ . ' ' . $call['n'] . ' '; + $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]]) + ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]]) + : '(unknown file)'); + $msg .= ' on line ' . $call[Parser::SOURCE_LINE]; + + $callStackMsg[] = $msg; + + if (! \is_null($limit) && $ncall > $limit) { + break; + } + } + } + } + + return implode("\n", $callStackMsg); + } + + /** + * Handle import loop + * + * @param string $name + * + * @return void + * + * @throws \Exception + */ + protected function handleImportLoop($name) + { + for ($env = $this->env; $env; $env = $env->parent) { + if (! $env->block) { + continue; + } + + $file = $this->sourceNames[$env->block->sourceIndex]; + + if ($file === null) { + continue; + } + + if (realpath($file) === $name) { + throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); + } + } + } + + /** + * Call SCSS @function + * + * @param CallableBlock|null $func + * @param array $argValues + * + * @return array|Number + */ + protected function callScssFunction($func, $argValues) + { + if (! $func) { + return static::$defaultValue; + } + $name = $func->name; + + $this->pushEnv(); + + // set the args + if (isset($func->args)) { + $this->applyArguments($func->args, $argValues); + } + + // throw away lines and children + $tmp = new OutputBlock(); + $tmp->lines = []; + $tmp->children = []; + + $this->env->marker = 'function'; + + if (! empty($func->parentEnv)) { + $this->env->declarationScopeParent = $func->parentEnv; + } else { + throw $this->error("@function $name() without parentEnv"); + } + + $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name); + + $this->popEnv(); + + return ! isset($ret) ? static::$defaultValue : $ret; + } + + /** + * Call built-in and registered (PHP) functions + * + * @param string $name + * @param callable $function + * @param array $prototype + * @param array $args + * + * @return array|Number|null + */ + protected function callNativeFunction($name, $function, $prototype, $args) + { + $libName = (is_array($function) ? end($function) : null); + $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); + + if (\is_null($sorted_kwargs)) { + return null; + } + @list($sorted, $kwargs) = $sorted_kwargs; + + if ($name !== 'if') { + foreach ($sorted as &$val) { + if ($val !== null) { + $val = $this->reduce($val, true); + } + } + } + + $returnValue = \call_user_func($function, $sorted, $kwargs); + + if (! isset($returnValue)) { + return null; + } + + if (\is_array($returnValue) || $returnValue instanceof Number) { + return $returnValue; + } + + @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED); + + return $this->coerceValue($returnValue); + } + + /** + * Get built-in function + * + * @param string $name Normalized name + * + * @return array + */ + protected function getBuiltinFunction($name) + { + $libName = self::normalizeNativeFunctionName($name); + return [$this, $libName]; + } + + /** + * Normalize native function name + * + * @internal + * + * @param string $name + * + * @return string + */ + public static function normalizeNativeFunctionName($name) + { + $name = str_replace("-", "_", $name); + $libName = 'lib' . preg_replace_callback( + '/_(.)/', + function ($m) { + return ucfirst($m[1]); + }, + ucfirst($name) + ); + return $libName; + } + + /** + * Check if a function is a native built-in scss function, for css parsing + * + * @internal + * + * @param string $name + * + * @return bool + */ + public static function isNativeFunction($name) + { + return method_exists(Compiler::class, self::normalizeNativeFunctionName($name)); + } + + /** + * Sorts keyword arguments + * + * @param string $functionName + * @param array|null $prototypes + * @param array $args + * + * @return array|null + */ + protected function sortNativeFunctionArgs($functionName, $prototypes, $args) + { + static $parser = null; + + if (! isset($prototypes)) { + $keyArgs = []; + $posArgs = []; + + if (\is_array($args) && \count($args) && \end($args) === static::$null) { + array_pop($args); + } + + // separate positional and keyword arguments + foreach ($args as $arg) { + list($key, $value) = $arg; + + if (empty($key) or empty($key[1])) { + $posArgs[] = empty($arg[2]) ? $value : $arg; + } else { + $keyArgs[$key[1]] = $value; + } + } + + return [$posArgs, $keyArgs]; + } + + // specific cases ? + if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { + // notation 100 127 255 / 0 is in fact a simple list of 4 values + foreach ($args as $k => $arg) { + if (!isset($arg[1])) { + continue; // This happens when using a trailing comma + } + if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { + $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]); + } + } + } + + list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false); + + if (! \is_array(reset($prototypes))) { + $prototypes = [$prototypes]; + } + + $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes); + assert(!empty($parsedPrototypes)); + $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names); + + $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat); + + $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator); + + $finalArgs = []; + $keyArgs = []; + + foreach ($matchedPrototype['arguments'] as $argument) { + list($normalizedName, $originalName, $default) = $argument; + + if (isset($vars[$normalizedName])) { + $value = $vars[$normalizedName]; + } else { + $value = $default; + } + + // special null value as default: translate to real null here + if ($value === [Type::T_KEYWORD, 'null']) { + $value = null; + } + + $finalArgs[] = $value; + $keyArgs[$originalName] = $value; + } + + if ($matchedPrototype['rest_argument'] !== null) { + $value = $vars[$matchedPrototype['rest_argument']]; + + $finalArgs[] = $value; + $keyArgs[$matchedPrototype['rest_argument']] = $value; + } + + return [$finalArgs, $keyArgs]; + } + + /** + * Parses a function prototype to the internal representation of arguments. + * + * The input is an array of strings describing each argument, as supported + * in {@see registerFunction}. Argument names don't include the `$`. + * The output contains the list of positional argument, with their normalized + * name (underscores are replaced by dashes), their original name (to be used + * in case of error reporting) and their default value. The output also contains + * the normalized name of the rest argument, or null if the function prototype + * is not variadic. + * + * @param string[] $prototype + * + * @return array + * @phpstan-return array{arguments: list, rest_argument: string|null} + */ + private function parseFunctionPrototype(array $prototype) + { + static $parser = null; + + $arguments = []; + $restArgument = null; + + foreach ($prototype as $p) { + if (null !== $restArgument) { + throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.'); + } + + $default = null; + $p = explode(':', $p, 2); + $name = str_replace('_', '-', $p[0]); + + if (isset($p[1])) { + $defaultSource = trim($p[1]); + + if ($defaultSource === 'null') { + // differentiate this null from the static::$null + $default = [Type::T_KEYWORD, 'null']; + } else { + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $parser->parseValue($defaultSource, $default); + } + } + + if (substr($name, -3) === '...') { + $restArgument = substr($name, 0, -3); + } else { + $arguments[] = [$name, $p[0], $default]; + } + } + + return [ + 'arguments' => $arguments, + 'rest_argument' => $restArgument, + ]; + } + + /** + * Returns the function prototype for the given positional and named arguments. + * + * If no exact match is found, finds the closest approximation. Note that this + * doesn't guarantee that $positional and $names are valid for the returned + * prototype. + * + * @param array[] $prototypes + * @param int $positional + * @param array $names A set of names, as both keys and values + * + * @return array + * + * @phpstan-param non-empty-list, rest_argument: string|null}> $prototypes + * @phpstan-return array{arguments: list, rest_argument: string|null} + */ + private function selectFunctionPrototype(array $prototypes, $positional, array $names) + { + $fuzzyMatch = null; + $minMismatchDistance = null; + + foreach ($prototypes as $prototype) { + // Ideally, find an exact match. + if ($this->checkPrototypeMatches($prototype, $positional, $names)) { + return $prototype; + } + + $mismatchDistance = \count($prototype['arguments']) - $positional; + + if ($minMismatchDistance !== null) { + if (abs($mismatchDistance) > abs($minMismatchDistance)) { + continue; + } + + // If two overloads have the same mismatch distance, favor the overload + // that has more arguments. + if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) { + continue; + } + } + + $minMismatchDistance = $mismatchDistance; + $fuzzyMatch = $prototype; + } + + return $fuzzyMatch; + } + + /** + * Checks whether the argument invocation matches the callable prototype. + * + * The rules are similar to {@see verifyPrototype}. The boolean return value + * avoids the overhead of building and catching exceptions when the reason of + * not matching the prototype does not need to be known. + * + * @param array $prototype + * @param int $positional + * @param array $names + * + * @return bool + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function checkPrototypeMatches(array $prototype, $positional, array $names) + { + $nameUsed = 0; + + foreach ($prototype['arguments'] as $i => $argument) { + list ($name, $originalName, $default) = $argument; + + if ($i < $positional) { + if (isset($names[$name])) { + return false; + } + } elseif (isset($names[$name])) { + $nameUsed++; + } elseif ($default === null) { + return false; + } + } + + if ($prototype['rest_argument'] !== null) { + return true; + } + + if ($positional > \count($prototype['arguments'])) { + return false; + } + + if ($nameUsed < \count($names)) { + return false; + } + + return true; + } + + /** + * Verifies that the argument invocation is valid for the callable prototype. + * + * @param array $prototype + * @param int $positional + * @param array $names + * @param bool $hasSplat + * + * @return void + * + * @throws SassScriptException + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat) + { + $nameUsed = 0; + + foreach ($prototype['arguments'] as $i => $argument) { + list ($name, $originalName, $default) = $argument; + + if ($i < $positional) { + if (isset($names[$name])) { + throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName)); + } + } elseif (isset($names[$name])) { + $nameUsed++; + } elseif ($default === null) { + throw new SassScriptException(sprintf('Missing argument $%s', $originalName)); + } + } + + if ($prototype['rest_argument'] !== null) { + return; + } + + if ($positional > \count($prototype['arguments'])) { + $message = sprintf( + 'Only %d %sargument%s allowed, but %d %s passed.', + \count($prototype['arguments']), + empty($names) ? '' : 'positional ', + \count($prototype['arguments']) === 1 ? '' : 's', + $positional, + $positional === 1 ? 'was' : 'were' + ); + if (!$hasSplat) { + throw new SassScriptException($message); + } + + $message = $this->addLocationToMessage($message); + $message .= "\nThis will be an error in future versions of Sass."; + $this->logger->warn($message, true); + } + + if ($nameUsed < \count($names)) { + $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0))); + $lastName = array_pop($unknownNames); + $message = sprintf( + 'No argument%s named $%s%s.', + $unknownNames ? 's' : '', + $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', + $lastName + ); + throw new SassScriptException($message); + } + } + + /** + * Evaluates the argument from the invocation. + * + * This returns several things about this invocation: + * - the list of positional arguments + * - the map of named arguments, indexed by normalized names + * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access) + * - the separator used by the list using the splat operator, if any + * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting. + * + * @param array[] $args + * @param bool $reduce Whether arguments should be reduced to their value + * + * @return array + * + * @throws SassScriptException + * + * @phpstan-return array{0: list, 1: array, 2: array, 3: string|null, 4: bool} + */ + private function evaluateArguments(array $args, $reduce = true) + { + // this represents trailing commas + if (\count($args) && end($args) === static::$null) { + array_pop($args); + } + + $splatSeparator = null; + $keywordArgs = []; + $names = []; + $positionalArgs = []; + $hasKeywordArgument = false; + $hasSplat = false; + + foreach ($args as $arg) { + if (!empty($arg[0])) { + $hasKeywordArgument = true; + + assert(\is_string($arg[0][1])); + $name = str_replace('_', '-', $arg[0][1]); + + if (isset($keywordArgs[$name])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1])); + } + + $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]); + $names[$name] = $name; + } elseif (! empty($arg[2])) { + // $arg[2] means a var followed by ... in the arg ($list... ) + $val = $this->reduce($arg[1], true); + $hasSplat = true; + + if ($val[0] === Type::T_LIST) { + foreach ($val[2] as $item) { + if (\is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $positionalArgs[] = $this->maybeReduce($reduce, $item); + } + + if (isset($val[3]) && \is_array($val[3])) { + foreach ($val[3] as $name => $item) { + assert(\is_string($name)); + + $normalizedName = str_replace('_', '-', $name); + + if (isset($keywordArgs[$normalizedName])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + } + + $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); + $names[$normalizedName] = $normalizedName; + $hasKeywordArgument = true; + } + } + } elseif ($val[0] === Type::T_MAP) { + foreach ($val[1] as $i => $name) { + $name = $this->compileStringContent($this->coerceString($name)); + $item = $val[2][$i]; + + if (! is_numeric($name)) { + $normalizedName = str_replace('_', '-', $name); + + if (isset($keywordArgs[$normalizedName])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + } + + $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); + $names[$normalizedName] = $normalizedName; + $hasKeywordArgument = true; + } else { + if (\is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $positionalArgs[] = $this->maybeReduce($reduce, $item); + } + } + } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list + $positionalArgs[] = $this->maybeReduce($reduce, $val); + } + } elseif ($hasKeywordArgument) { + throw new SassScriptException('Positional arguments must come before keyword arguments.'); + } else { + $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]); + } + } + + return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat]; + } + + /** + * @param bool $reduce + * @param array|Number $value + * + * @return array|Number + */ + private function maybeReduce($reduce, $value) + { + if ($reduce) { + return $this->reduce($value, true); + } + + return $value; + } + + /** + * Apply argument values per definition + * + * @param array[] $argDef + * @param array|null $argValues + * @param bool $storeInEnv + * @param bool $reduce only used if $storeInEnv = false + * + * @return array + * + * @phpstan-param list $argDef + * + * @throws \Exception + */ + protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) + { + $output = []; + + if (\is_null($argValues)) { + $argValues = []; + } + + if ($storeInEnv) { + $storeEnv = $this->getStoreEnv(); + + $env = new Environment(); + $env->store = $storeEnv->store; + } + + $prototype = ['arguments' => [], 'rest_argument' => null]; + $originalRestArgumentName = null; + + foreach ($argDef as $arg) { + list($name, $default, $isVariable) = $arg; + $normalizedName = str_replace('_', '-', $name); + + if ($isVariable) { + $originalRestArgumentName = $name; + $prototype['rest_argument'] = $normalizedName; + } else { + $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null]; + } + } + + list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce); + + $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat); + + $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator); + + foreach ($prototype['arguments'] as $argument) { + list($normalizedName, $name) = $argument; + + if (!isset($vars[$normalizedName])) { + continue; + } + + $val = $vars[$normalizedName]; + + if ($storeInEnv) { + $this->set($name, $this->reduce($val, true), true, $env); + } else { + $output[$name] = ($reduce ? $this->reduce($val, true) : $val); + } + } + + if ($prototype['rest_argument'] !== null) { + assert($originalRestArgumentName !== null); + $name = $originalRestArgumentName; + $val = $vars[$prototype['rest_argument']]; + + if ($storeInEnv) { + $this->set($name, $this->reduce($val, true), true, $env); + } else { + $output[$name] = ($reduce ? $this->reduce($val, true) : $val); + } + } + + if ($storeInEnv) { + $storeEnv->store = $env->store; + } + + foreach ($prototype['arguments'] as $argument) { + list($normalizedName, $name, $default) = $argument; + + if (isset($vars[$normalizedName])) { + continue; + } + assert($default !== null); + + if ($storeInEnv) { + $this->set($name, $this->reduce($default, true), true); + } else { + $output[$name] = ($reduce ? $this->reduce($default, true) : $default); + } + } + + return $output; + } + + /** + * Apply argument values per definition. + * + * This method assumes that the arguments are valid for the provided prototype. + * The validation with {@see verifyPrototype} must have been run before calling + * it. + * Arguments are returned as a map from the normalized argument names to the + * value. Additional arguments are collected in a sass argument list available + * under the name of the rest argument in the result. + * + * Defaults are not applied as they are resolved in a different environment. + * + * @param array $prototype + * @param array $positionalArgs + * @param array $namedArgs + * @param string|null $splatSeparator + * + * @return array + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator) + { + $output = []; + $minLength = min(\count($positionalArgs), \count($prototype['arguments'])); + + for ($i = 0; $i < $minLength; $i++) { + list($name) = $prototype['arguments'][$i]; + $val = $positionalArgs[$i]; + + $output[$name] = $val; + } + + $restNamed = $namedArgs; + + for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) { + $argument = $prototype['arguments'][$i]; + list($name) = $argument; + + if (isset($namedArgs[$name])) { + $val = $namedArgs[$name]; + unset($restNamed[$name]); + } else { + continue; + } + + $output[$name] = $val; + } + + if ($prototype['rest_argument'] !== null) { + $name = $prototype['rest_argument']; + $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments']))); + + $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed]; + + $output[$name] = $val; + } + + return $output; + } + + /** + * Coerce a php value into a scss one + * + * @param mixed $value + * + * @return array|Number + */ + protected function coerceValue($value) + { + if (\is_array($value) || $value instanceof Number) { + return $value; + } + + if (\is_bool($value)) { + return $this->toBool($value); + } + + if (\is_null($value)) { + return static::$null; + } + + if (is_numeric($value)) { + return new Number($value, ''); + } + + if ($value === '') { + return static::$emptyString; + } + + $value = [Type::T_KEYWORD, $value]; + $color = $this->coerceColor($value); + + if ($color) { + return $color; + } + + return $value; + } + + /** + * Tries to convert an item to a Sass map + * + * @param Number|array $item + * + * @return array|null + */ + private function tryMap($item) + { + if ($item instanceof Number) { + return null; + } + + if ($item[0] === Type::T_MAP) { + return $item; + } + + if ( + $item[0] === Type::T_LIST && + $item[2] === [] + ) { + return static::$emptyMap; + } + + return null; + } + + /** + * Coerce something to map + * + * @param array|Number $item + * + * @return array|Number + */ + protected function coerceMap($item) + { + $map = $this->tryMap($item); + + if ($map !== null) { + return $map; + } + + return $item; + } + + /** + * Coerce something to list + * + * @param array|Number $item + * @param string $delim + * @param bool $removeTrailingNull + * + * @return array + */ + protected function coerceList($item, $delim = ',', $removeTrailingNull = false) + { + if ($item instanceof Number) { + return [Type::T_LIST, '', [$item]]; + } + + if ($item[0] === Type::T_LIST) { + // remove trailing null from the list + if ($removeTrailingNull && end($item[2]) === static::$null) { + array_pop($item[2]); + } + + return $item; + } + + if ($item[0] === Type::T_MAP) { + $keys = $item[1]; + $values = $item[2]; + $list = []; + + for ($i = 0, $s = \count($keys); $i < $s; $i++) { + $key = $keys[$i]; + $value = $values[$i]; + + $list[] = [ + Type::T_LIST, + ' ', + [$key, $value] + ]; + } + + return [Type::T_LIST, $list ? ',' : '', $list]; + } + + return [Type::T_LIST, '', [$item]]; + } + + /** + * Coerce color for expression + * + * @param array|Number $value + * + * @return array|Number + */ + protected function coerceForExpression($value) + { + if ($color = $this->coerceColor($value)) { + return $color; + } + + return $value; + } + + /** + * Coerce value to color + * + * @param array|Number $value + * @param bool $inRGBFunction + * + * @return array|null + */ + protected function coerceColor($value, $inRGBFunction = false) + { + if ($value instanceof Number) { + return null; + } + + switch ($value[0]) { + case Type::T_COLOR: + for ($i = 1; $i <= 3; $i++) { + if (! is_numeric($value[$i])) { + $cv = $this->compileRGBAValue($value[$i]); + + if (! is_numeric($cv)) { + return null; + } + + $value[$i] = $cv; + } + + if (isset($value[4])) { + if (! is_numeric($value[4])) { + $cv = $this->compileRGBAValue($value[4], true); + + if (! is_numeric($cv)) { + return null; + } + + $value[4] = $cv; + } + } + } + + return $value; + + case Type::T_LIST: + if ($inRGBFunction) { + if (\count($value[2]) == 3 || \count($value[2]) == 4) { + $color = $value[2]; + array_unshift($color, Type::T_COLOR); + + return $this->coerceColor($color); + } + } + + return null; + + case Type::T_KEYWORD: + if (! \is_string($value[1])) { + return null; + } + + $name = strtolower($value[1]); + + // hexa color? + if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) { + $nofValues = \strlen($m[1]); + + if (\in_array($nofValues, [3, 4, 6, 8])) { + $nbChannels = 3; + $color = []; + $num = hexdec($m[1]); + + switch ($nofValues) { + case 4: + $nbChannels = 4; + // then continuing with the case 3: + case 3: + for ($i = 0; $i < $nbChannels; $i++) { + $t = $num & 0xf; + array_unshift($color, $t << 4 | $t); + $num >>= 4; + } + + break; + + case 8: + $nbChannels = 4; + // then continuing with the case 6: + case 6: + for ($i = 0; $i < $nbChannels; $i++) { + array_unshift($color, $num & 0xff); + $num >>= 8; + } + + break; + } + + if ($nbChannels === 4) { + if ($color[3] === 255) { + $color[3] = 1; // fully opaque + } else { + $color[3] = round($color[3] / 255, Number::PRECISION); + } + } + + array_unshift($color, Type::T_COLOR); + + return $color; + } + } + + if ($rgba = Colors::colorNameToRGBa($name)) { + return isset($rgba[3]) + ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]] + : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]]; + } + + return null; + } + + return null; + } + + /** + * @param int|Number $value + * @param bool $isAlpha + * + * @return int|mixed + */ + protected function compileRGBAValue($value, $isAlpha = false) + { + if ($isAlpha) { + return $this->compileColorPartValue($value, 0, 1, false); + } + + return $this->compileColorPartValue($value, 0, 255, true); + } + + /** + * @param mixed $value + * @param int|float $min + * @param int|float $max + * @param bool $isInt + * + * @return int|mixed + */ + protected function compileColorPartValue($value, $min, $max, $isInt = true) + { + if (! is_numeric($value)) { + if (\is_array($value)) { + $reduced = $this->reduce($value); + + if ($reduced instanceof Number) { + $value = $reduced; + } + } + + if ($value instanceof Number) { + if ($value->unitless()) { + $num = $value->getDimension(); + } elseif ($value->hasUnit('%')) { + $num = $max * $value->getDimension() / 100; + } else { + throw $this->error('Expected %s to have no units or "%%".', $value); + } + + $value = $num; + } elseif (\is_array($value)) { + $value = $this->compileValue($value); + } + } + + if (is_numeric($value)) { + if ($isInt) { + $value = round($value); + } + + $value = min($max, max($min, $value)); + + return $value; + } + + return $value; + } + + /** + * Coerce value to string + * + * @param array|Number $value + * + * @return array + */ + protected function coerceString($value) + { + if ($value[0] === Type::T_STRING) { + assert(\is_array($value)); + + return $value; + } + + return [Type::T_STRING, '', [$this->compileValue($value)]]; + } + + /** + * Assert value is a string + * + * This method deals with internal implementation details of the value + * representation where unquoted strings can sometimes be stored under + * other types. + * The returned value is always using the T_STRING type. + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return array + * + * @throws SassScriptException + */ + public function assertString($value, $varName = null) + { + // case of url(...) parsed a a function + if ($value[0] === Type::T_FUNCTION) { + $value = $this->coerceString($value); + } + + if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) { + $value = $this->compileValue($value); + throw SassScriptException::forArgument("$value is not a string.", $varName); + } + + return $this->coerceString($value); + } + + /** + * Coerce value to a percentage + * + * @param array|Number $value + * + * @return int|float + * + * @deprecated + */ + protected function coercePercent($value) + { + @trigger_error(sprintf('"%s" is deprecated since 1.7.0.', __METHOD__), E_USER_DEPRECATED); + + if ($value instanceof Number) { + if ($value->hasUnit('%')) { + return $value->getDimension() / 100; + } + + return $value->getDimension(); + } + + return 0; + } + + /** + * Assert value is a map + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return array + * + * @throws SassScriptException + */ + public function assertMap($value, $varName = null) + { + $map = $this->tryMap($value); + + if ($map === null) { + $value = $this->compileValue($value); + + throw SassScriptException::forArgument("$value is not a map.", $varName); + } + + return $map; + } + + /** + * Assert value is a list + * + * @api + * + * @param array|Number $value + * + * @return array + * + * @throws \Exception + */ + public function assertList($value) + { + if ($value[0] !== Type::T_LIST) { + throw $this->error('expecting list, %s received', $value[0]); + } + assert(\is_array($value)); + + return $value; + } + + /** + * Gets the keywords of an argument list. + * + * Keys in the returned array are normalized names (underscores are replaced with dashes) + * without the leading `$`. + * Calling this helper with anything that an argument list received for a rest argument + * of the function argument declaration is not supported. + * + * @param array|Number $value + * + * @return array + */ + public function getArgumentListKeywords($value) + { + if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { + throw new \InvalidArgumentException('The argument is not a sass argument list.'); + } + + return $value[3]; + } + + /** + * Assert value is a color + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return array + * + * @throws SassScriptException + */ + public function assertColor($value, $varName = null) + { + if ($color = $this->coerceColor($value)) { + return $color; + } + + $value = $this->compileValue($value); + + throw SassScriptException::forArgument("$value is not a color.", $varName); + } + + /** + * Assert value is a number + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return Number + * + * @throws SassScriptException + */ + public function assertNumber($value, $varName = null) + { + if (!$value instanceof Number) { + $value = $this->compileValue($value); + throw SassScriptException::forArgument("$value is not a number.", $varName); + } + + return $value; + } + + /** + * Assert value is a integer + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return int + * + * @throws SassScriptException + */ + public function assertInteger($value, $varName = null) + { + $value = $this->assertNumber($value, $varName)->getDimension(); + if (round($value - \intval($value), Number::PRECISION) > 0) { + throw SassScriptException::forArgument("$value is not an integer.", $varName); + } + + return intval($value); + } + + /** + * Extract the ... / alpha on the last argument of channel arg + * in color functions + * + * @param array $args + * @return array + */ + private function extractSlashAlphaInColorFunction($args) + { + $last = end($args); + if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') { + array_pop($args); + $args[] = $last[2]; + $args[] = $last[3]; + } + return $args; + } + + + /** + * Make sure a color's components don't go out of bounds + * + * @param array $c + * + * @return array + */ + protected function fixColor($c) + { + foreach ([1, 2, 3] as $i) { + if ($c[$i] < 0) { + $c[$i] = 0; + } + + if ($c[$i] > 255) { + $c[$i] = 255; + } + + if (!\is_int($c[$i])) { + $c[$i] = round($c[$i]); + } + } + + return $c; + } + + /** + * Convert RGB to HSL + * + * @internal + * + * @param int $red + * @param int $green + * @param int $blue + * + * @return array + */ + public function toHSL($red, $green, $blue) + { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $l = $min + $max; + $d = $max - $min; + + if ((int) $d === 0) { + $h = $s = 0; + } else { + if ($l < 255) { + $s = $d / $l; + } else { + $s = $d / (510 - $l); + } + + if ($red == $max) { + $h = 60 * ($green - $blue) / $d; + } elseif ($green == $max) { + $h = 60 * ($blue - $red) / $d + 120; + } else { + $h = 60 * ($red - $green) / $d + 240; + } + } + + return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1]; + } + + /** + * Hue to RGB helper + * + * @param float $m1 + * @param float $m2 + * @param float $h + * + * @return float + */ + protected function hueToRGB($m1, $m2, $h) + { + if ($h < 0) { + $h += 1; + } elseif ($h > 1) { + $h -= 1; + } + + if ($h * 6 < 1) { + return $m1 + ($m2 - $m1) * $h * 6; + } + + if ($h * 2 < 1) { + return $m2; + } + + if ($h * 3 < 2) { + return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6; + } + + return $m1; + } + + /** + * Convert HSL to RGB + * + * @internal + * + * @param int|float $hue H from 0 to 360 + * @param int|float $saturation S from 0 to 100 + * @param int|float $lightness L from 0 to 100 + * + * @return array + */ + public function toRGB($hue, $saturation, $lightness) + { + if ($hue < 0) { + $hue += 360; + } + + $h = $hue / 360; + $s = min(100, max(0, $saturation)) / 100; + $l = min(100, max(0, $lightness)) / 100; + + $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; + $m1 = $l * 2 - $m2; + + $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255; + $g = $this->hueToRGB($m1, $m2, $h) * 255; + $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255; + + $out = [Type::T_COLOR, $r, $g, $b]; + + return $out; + } + + /** + * Convert HWB to RGB + * https://www.w3.org/TR/css-color-4/#hwb-to-rgb + * + * @api + * + * @param int|float $hue H from 0 to 360 + * @param int|float $whiteness W from 0 to 100 + * @param int|float $blackness B from 0 to 100 + * + * @return array + */ + private function HWBtoRGB($hue, $whiteness, $blackness) + { + $w = min(100, max(0, $whiteness)) / 100; + $b = min(100, max(0, $blackness)) / 100; + + $sum = $w + $b; + if ($sum > 1.0) { + $w = $w / $sum; + $b = $b / $sum; + } + $b = min(1.0 - $w, $b); + + $rgb = $this->toRGB($hue, 100, 50); + for($i = 1; $i < 4; $i++) { + $rgb[$i] *= (1.0 - $w - $b); + $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001); + } + + return $rgb; + } + + /** + * Convert RGB to HWB + * + * @api + * + * @param int $red + * @param int $green + * @param int $blue + * + * @return array + */ + private function RGBtoHWB($red, $green, $blue) + { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $d = $max - $min; + + if ((int) $d === 0) { + $h = 0; + } else { + + if ($red == $max) { + $h = 60 * ($green - $blue) / $d; + } elseif ($green == $max) { + $h = 60 * ($blue - $red) / $d + 120; + } else { + $h = 60 * ($red - $green) / $d + 240; + } + } + + return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100]; + } + + + // Built in functions + + protected static $libCall = ['function', 'args...']; + protected function libCall($args) + { + $functionReference = $args[0]; + + if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) { + $name = $this->compileStringContent($this->coerceString($functionReference)); + $warning = "Passing a string to call() is deprecated and will be illegal\n" + . "in Sass 4.0. Use call(function-reference($name)) instead."; + Warn::deprecation($warning); + $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]); + } + + if ($functionReference === static::$null) { + return static::$null; + } + + if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) { + throw $this->error('Function reference expected, got ' . $functionReference[0]); + } + + $callArgs = [ + [null, $args[1], true] + ]; + + return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); + } + + + protected static $libGetFunction = [ + ['name'], + ['name', 'css'] + ]; + protected function libGetFunction($args) + { + $name = $this->compileStringContent($this->assertString(array_shift($args), 'name')); + $isCss = false; + + if (count($args)) { + $isCss = array_shift($args); + $isCss = (($isCss === static::$true) ? true : false); + } + + if ($isCss) { + return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; + } + + return $this->getFunctionReference($name, true); + } + + protected static $libIf = ['condition', 'if-true', 'if-false:']; + protected function libIf($args) + { + list($cond, $t, $f) = $args; + + if (! $this->isTruthy($this->reduce($cond, true))) { + return $this->reduce($f, true); + } + + return $this->reduce($t, true); + } + + protected static $libIndex = ['list', 'value']; + protected function libIndex($args) + { + list($list, $value) = $args; + + if ( + $list[0] === Type::T_MAP || + $list[0] === Type::T_STRING || + $list[0] === Type::T_KEYWORD || + $list[0] === Type::T_INTERPOLATE + ) { + $list = $this->coerceList($list, ' '); + } + + if ($list[0] !== Type::T_LIST) { + return static::$null; + } + + // Numbers are represented with value objects, for which the PHP equality operator does not + // match the Sass rules (and we cannot overload it). As they are the only type of values + // represented with a value object for now, they require a special case. + if ($value instanceof Number) { + $key = 0; + foreach ($list[2] as $item) { + $key++; + $itemValue = $this->normalizeValue($item); + + if ($itemValue instanceof Number && $value->equals($itemValue)) { + return new Number($key, ''); + } + } + return static::$null; + } + + $values = []; + + foreach ($list[2] as $item) { + $values[] = $this->normalizeValue($item); + } + + $key = array_search($this->normalizeValue($value), $values); + + return false === $key ? static::$null : new Number($key + 1, ''); + } + + protected static $libRgb = [ + ['color'], + ['color', 'alpha'], + ['channels'], + ['red', 'green', 'blue'], + ['red', 'green', 'blue', 'alpha'] ]; + + /** + * @param array $args + * @param array $kwargs + * @param string $funcName + * + * @return array + */ + protected function libRgb($args, $kwargs, $funcName = 'rgb') + { + switch (\count($args)) { + case 1: + if (! $color = $this->coerceColor($args[0], true)) { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } + break; + + case 3: + $color = [Type::T_COLOR, $args[0], $args[1], $args[2]]; + + if (! $color = $this->coerceColor($color)) { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + } + + return $color; + + case 2: + if ($color = $this->coerceColor($args[0], true)) { + $alpha = $this->compileRGBAValue($args[1], true); + + if (is_numeric($alpha)) { + $color[4] = $alpha; + } else { + $color = [Type::T_STRING, '', + [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']]; + } + } else { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']]; + } + break; + + case 4: + default: + $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]]; + + if (! $color = $this->coerceColor($color)) { + $color = [Type::T_STRING, '', + [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; + } + break; + } + + return $color; + } + + protected static $libRgba = [ + ['color'], + ['color', 'alpha'], + ['channels'], + ['red', 'green', 'blue'], + ['red', 'green', 'blue', 'alpha'] ]; + protected function libRgba($args, $kwargs) + { + return $this->libRgb($args, $kwargs, 'rgba'); + } + + /** + * Helper function for adjust_color, change_color, and scale_color + * + * @param array $args + * @param string $operation + * @param callable $fn + * + * @return array + * + * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn + */ + protected function alterColor(array $args, $operation, $fn) + { + $color = $this->assertColor($args[0], 'color'); + + if ($args[1][2]) { + throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.'); + } + + $kwargs = $this->getArgumentListKeywords($args[1]); + + $scale = $operation === 'scale'; + $change = $operation === 'change'; + + /** @phpstan-var callable(string, float|int, bool=, bool=): (float|int|null) $getParam */ + $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) { + if (!isset($kwargs[$name])) { + return null; + } + + $number = $this->assertNumber($kwargs[$name], $name); + unset($kwargs[$name]); + + if (!$scale && $checkPercent) { + if (!$number->hasUnit('%')) { + $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated."); + $this->logger->warn($warning->getMessage(), true); + } + } + + if ($scale || $assertPercent) { + $number->assertUnit('%', $name); + } + + if ($scale) { + $max = 100; + } + + if ($scale || $assertPercent) { + return $number->valueInRange($change ? 0 : -$max, $max, $name); + } + + return $number->valueInRangeWithUnit($change ? 0 : -$max, $max, $name, $checkPercent ? '%' : ''); + }; + + $alpha = $getParam('alpha', 1); + $red = $getParam('red', 255); + $green = $getParam('green', 255); + $blue = $getParam('blue', 255); + + if ($scale || !isset($kwargs['hue'])) { + $hue = null; + } else { + $hueNumber = $this->assertNumber($kwargs['hue'], 'hue'); + unset($kwargs['hue']); + $hue = $hueNumber->getDimension(); + } + $saturation = $getParam('saturation', 100, true); + $lightness = $getParam('lightness', 100, true); + $whiteness = $getParam('whiteness', 100, false, true); + $blackness = $getParam('blackness', 100, false, true); + + if (!empty($kwargs)) { + $unknownNames = array_keys($kwargs); + $lastName = array_pop($unknownNames); + $message = sprintf( + 'No argument%s named $%s%s.', + $unknownNames ? 's' : '', + $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', + $lastName + ); + throw new SassScriptException($message); + } + + $hasRgb = $red !== null || $green !== null || $blue !== null; + $hasSL = $saturation !== null || $lightness !== null; + $hasWB = $whiteness !== null || $blackness !== null; + + if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) { + throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL')); + } + + if ($hasWB && $hasSL) { + throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.'); + } + + if ($hasRgb) { + $color[1] = round($fn($color[1], $red, 255)); + $color[2] = round($fn($color[2], $green, 255)); + $color[3] = round($fn($color[3], $blue, 255)); + } elseif ($hasWB) { + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + if ($hue !== null) { + $hwb[1] = $change ? $hue : $hwb[1] + $hue; + } + $hwb[2] = $fn($hwb[2], $whiteness, 100); + $hwb[3] = $fn($hwb[3], $blackness, 100); + + $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]); + + if (isset($color[4])) { + $rgb[4] = $color[4]; + } + + $color = $rgb; + } elseif ($hue !== null || $hasSL) { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + if ($hue !== null) { + $hsl[1] = $change ? $hue : $hsl[1] + $hue; + } + $hsl[2] = $fn($hsl[2], $saturation, 100); + $hsl[3] = $fn($hsl[3], $lightness, 100); + + $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + + if (isset($color[4])) { + $rgb[4] = $color[4]; + } + + $color = $rgb; + } + + if ($alpha !== null) { + $existingAlpha = isset($color[4]) ? $color[4] : 1; + $color[4] = $fn($existingAlpha, $alpha, 1); + } + + return $color; + } + + protected static $libAdjustColor = ['color', 'kwargs...']; + protected function libAdjustColor($args) + { + return $this->alterColor($args, 'adjust', function ($base, $alter, $max) { + if ($alter === null) { + return $base; + } + + $new = $base + $alter; + + if ($new < 0) { + return 0; + } + + if ($new > $max) { + return $max; + } + + return $new; + }); + } + + protected static $libChangeColor = ['color', 'kwargs...']; + protected function libChangeColor($args) + { + return $this->alterColor($args,'change', function ($base, $alter, $max) { + if ($alter === null) { + return $base; + } + + return $alter; + }); + } + + protected static $libScaleColor = ['color', 'kwargs...']; + protected function libScaleColor($args) + { + return $this->alterColor($args, 'scale', function ($base, $scale, $max) { + if ($scale === null) { + return $base; + } + + $scale = $scale / 100; + + if ($scale < 0) { + return $base * $scale + $base; + } + + return ($max - $base) * $scale + $base; + }); + } + + protected static $libIeHexStr = ['color']; + protected function libIeHexStr($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color'); + } + + $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255; + + return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; + } + + protected static $libRed = ['color']; + protected function libRed($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `red($color)` must be a color'); + } + + return new Number((int) $color[1], ''); + } + + protected static $libGreen = ['color']; + protected function libGreen($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `green($color)` must be a color'); + } + + return new Number((int) $color[2], ''); + } + + protected static $libBlue = ['color']; + protected function libBlue($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `blue($color)` must be a color'); + } + + return new Number((int) $color[3], ''); + } + + protected static $libAlpha = ['color']; + protected function libAlpha($args) + { + if ($color = $this->coerceColor($args[0])) { + return new Number(isset($color[4]) ? $color[4] : 1, ''); + } + + // this might be the IE function, so return value unchanged + return null; + } + + protected static $libOpacity = ['color']; + protected function libOpacity($args) + { + $value = $args[0]; + + if ($value instanceof Number) { + return null; + } + + return $this->libAlpha($args); + } + + // mix two colors + protected static $libMix = [ + ['color1', 'color2', 'weight:50%'], + ['color-1', 'color-2', 'weight:50%'] + ]; + protected function libMix($args) + { + list($first, $second, $weight) = $args; + + $first = $this->assertColor($first, 'color1'); + $second = $this->assertColor($second, 'color2'); + $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100; + + $firstAlpha = isset($first[4]) ? $first[4] : 1; + $secondAlpha = isset($second[4]) ? $second[4] : 1; + + $normalizedWeight = $weightScale * 2 - 1; + $alphaDistance = $firstAlpha - $secondAlpha; + + $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance); + $weight1 = ($combinedWeight + 1) / 2.0; + $weight2 = 1.0 - $weight1; + + $new = [Type::T_COLOR, + $weight1 * $first[1] + $weight2 * $second[1], + $weight1 * $first[2] + $weight2 * $second[2], + $weight1 * $first[3] + $weight2 * $second[3], + ]; + + if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { + $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale); + } + + return $this->fixColor($new); + } + + protected static $libHsl = [ + ['channels'], + ['hue', 'saturation'], + ['hue', 'saturation', 'lightness'], + ['hue', 'saturation', 'lightness', 'alpha'] ]; + + /** + * @param array $args + * @param array $kwargs + * @param string $funcName + * + * @return array|null + */ + protected function libHsl($args, $kwargs, $funcName = 'hsl') + { + $args_to_check = $args; + + if (\count($args) == 1) { + if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) { + return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } + + $args = $args[0][2]; + $args_to_check = $kwargs['channels'][2]; + } + + if (\count($args) === 2) { + // if var() is used as an argument, return as a css function + foreach ($args as $arg) { + if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) { + return null; + } + } + + throw new SassScriptException('Missing argument $lightness.'); + } + + foreach ($kwargs as $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { + return null; + } + } + + foreach ($args_to_check as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { + if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { + return null; + } + + $args[$k] = $this->stringifyFncallArgs($arg); + } + + if ( + $k >= 2 && count($args) === 4 && + in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && + in_array($arg[1], ['calc','env']) + ) { + return null; + } + } + + $hue = $this->reduce($args[0]); + $saturation = $this->reduce($args[1]); + $lightness = $this->reduce($args[2]); + $alpha = null; + + if (\count($args) === 4) { + $alpha = $this->compileColorPartValue($args[3], 0, 100, false); + + if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) { + return [Type::T_STRING, '', + [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; + } + } else { + if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) { + return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + } + } + + $hueValue = fmod($hue->getDimension(), 360); + + while ($hueValue < 0) { + $hueValue += 360; + } + + $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100))); + + if (! \is_null($alpha)) { + $color[4] = $alpha; + } + + return $color; + } + + protected static $libHsla = [ + ['channels'], + ['hue', 'saturation'], + ['hue', 'saturation', 'lightness'], + ['hue', 'saturation', 'lightness', 'alpha']]; + protected function libHsla($args, $kwargs) + { + return $this->libHsl($args, $kwargs, 'hsla'); + } + + protected static $libHue = ['color']; + protected function libHue($args) + { + $color = $this->assertColor($args[0], 'color'); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Number($hsl[1], 'deg'); + } + + protected static $libSaturation = ['color']; + protected function libSaturation($args) + { + $color = $this->assertColor($args[0], 'color'); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Number($hsl[2], '%'); + } + + protected static $libLightness = ['color']; + protected function libLightness($args) + { + $color = $this->assertColor($args[0], 'color'); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Number($hsl[3], '%'); + } + + /* + * Todo : a integrer dans le futur module color + protected static $libHwb = [ + ['channels'], + ['hue', 'whiteness', 'blackness'], + ['hue', 'whiteness', 'blackness', 'alpha'] ]; + protected function libHwb($args, $kwargs, $funcName = 'hwb') + { + $args_to_check = $args; + + if (\count($args) == 1) { + if ($args[0][0] !== Type::T_LIST) { + throw $this->error("Missing elements \$whiteness and \$blackness"); + } + + if (\trim($args[0][1])) { + throw $this->error("\$channels must be a space-separated list."); + } + + if (! empty($args[0]['enclosing'])) { + throw $this->error("\$channels must be an unbracketed list."); + } + + $args = $args[0][2]; + if (\count($args) > 3) { + throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed"); + } + + $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]); + if (\count($args_to_check) !== \count($kwargs['channels'][2])) { + $args = $args_to_check; + } + } + + if (\count($args_to_check) < 2) { + throw $this->error("Missing elements \$whiteness and \$blackness"); + } + if (\count($args_to_check) < 3) { + throw $this->error("Missing element \$blackness"); + } + if (\count($args_to_check) > 4) { + throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed"); + } + + foreach ($kwargs as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + return null; + } + } + + foreach ($args_to_check as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { + return null; + } + + $args[$k] = $this->stringifyFncallArgs($arg); + } + + if ( + $k >= 2 && count($args) === 4 && + in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && + in_array($arg[1], ['calc','env']) + ) { + return null; + } + } + + $hue = $this->reduce($args[0]); + $whiteness = $this->reduce($args[1]); + $blackness = $this->reduce($args[2]); + $alpha = null; + + if (\count($args) === 4) { + $alpha = $this->compileColorPartValue($args[3], 0, 1, false); + + if (! \is_numeric($alpha)) { + $val = $this->compileValue($args[3]); + throw $this->error("\$alpha: $val is not a number"); + } + } + + $this->assertNumber($hue, 'hue'); + $this->assertUnit($whiteness, ['%'], 'whiteness'); + $this->assertUnit($blackness, ['%'], 'blackness'); + + $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness"); + $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness"); + + $w = $whiteness->getDimension(); + $b = $blackness->getDimension(); + + $hueValue = $hue->getDimension() % 360; + + while ($hueValue < 0) { + $hueValue += 360; + } + + $color = $this->HWBtoRGB($hueValue, $w, $b); + + if (! \is_null($alpha)) { + $color[4] = $alpha; + } + + return $color; + } + + protected static $libWhiteness = ['color']; + protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') { + + $color = $this->assertColor($args[0]); + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + + return new Number($hwb[2], '%'); + } + + protected static $libBlackness = ['color']; + protected function libBlackness($args, $kwargs, $funcName = 'blackness') { + + $color = $this->assertColor($args[0]); + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + + return new Number($hwb[3], '%'); + } + */ + + /** + * @param array $color + * @param int $idx + * @param int|float $amount + * + * @return array + */ + protected function adjustHsl($color, $idx, $amount) + { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + $hsl[$idx] += $amount; + + if ($idx !== 1) { + // Clamp the saturation and lightness + $hsl[$idx] = min(max(0, $hsl[$idx]), 100); + } + + $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + + if (isset($color[4])) { + $out[4] = $color[4]; + } + + return $out; + } + + protected static $libAdjustHue = ['color', 'degrees']; + protected function libAdjustHue($args) + { + $color = $this->assertColor($args[0], 'color'); + $degrees = $this->assertNumber($args[1], 'degrees')->getDimension(); + + return $this->adjustHsl($color, 1, $degrees); + } + + protected static $libLighten = ['color', 'amount']; + protected function libLighten($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); + + return $this->adjustHsl($color, 3, $amount); + } + + protected static $libDarken = ['color', 'amount']; + protected function libDarken($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); + + return $this->adjustHsl($color, 3, -$amount); + } + + protected static $libSaturate = [['color', 'amount'], ['amount']]; + protected function libSaturate($args) + { + $value = $args[0]; + + if (count($args) === 1) { + $this->assertNumber($args[0], 'amount'); + + return null; + } + + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount')); + } + + protected static $libDesaturate = ['color', 'amount']; + protected function libDesaturate($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount')); + } + + protected static $libGrayscale = ['color']; + protected function libGrayscale($args) + { + $value = $args[0]; + + if ($value instanceof Number) { + return null; + } + + return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100); + } + + protected static $libComplement = ['color']; + protected function libComplement($args) + { + return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180); + } + + protected static $libInvert = ['color', 'weight:100%']; + protected function libInvert($args) + { + $value = $args[0]; + + $weight = $this->assertNumber($args[1], 'weight'); + + if ($value instanceof Number) { + if ($weight->getDimension() != 100 || !$weight->hasUnit('%')) { + throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.'); + } + + return null; + } + + $color = $this->assertColor($value, 'color'); + $inverted = $color; + $inverted[1] = 255 - $inverted[1]; + $inverted[2] = 255 - $inverted[2]; + $inverted[3] = 255 - $inverted[3]; + + return $this->libMix([$inverted, $color, $weight]); + } + + // increases opacity by amount + protected static $libOpacify = ['color', 'amount']; + protected function libOpacify($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRangeWithUnit(0, 1, 'amount', ''); + $color[4] = min(1, max(0, $color[4])); + + return $color; + } + + protected static $libFadeIn = ['color', 'amount']; + protected function libFadeIn($args) + { + return $this->libOpacify($args); + } + + // decreases opacity by amount + protected static $libTransparentize = ['color', 'amount']; + protected function libTransparentize($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRangeWithUnit(0, 1, 'amount', ''); + $color[4] = min(1, max(0, $color[4])); + + return $color; + } + + protected static $libFadeOut = ['color', 'amount']; + protected function libFadeOut($args) + { + return $this->libTransparentize($args); + } + + protected static $libUnquote = ['string']; + protected function libUnquote($args) + { + try { + $str = $this->assertString($args[0], 'string'); + } catch (SassScriptException $e) { + $value = $this->compileValue($args[0]); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + $message = "Passing $value, a non-string value, to unquote() +will be an error in future versions of Sass.\n on line $line of $fname"; + + $this->logger->warn($message, true); + + return $args[0]; + } + + $str[1] = ''; + + return $str; + } + + protected static $libQuote = ['string']; + protected function libQuote($args) + { + $value = $this->assertString($args[0], 'string'); + + $value[1] = '"'; + + return $value; + } + + protected static $libPercentage = ['number']; + protected function libPercentage($args) + { + $num = $this->assertNumber($args[0], 'number'); + $num->assertNoUnits('number'); + + return new Number($num->getDimension() * 100, '%'); + } + + protected static $libRound = ['number']; + protected function libRound($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libFloor = ['number']; + protected function libFloor($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libCeil = ['number']; + protected function libCeil($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libAbs = ['number']; + protected function libAbs($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libMin = ['numbers...']; + protected function libMin($args) + { + /** + * @var Number|null + */ + $min = null; + + foreach ($args[0][2] as $arg) { + $number = $this->assertNumber($arg); + + if (\is_null($min) || $min->greaterThan($number)) { + $min = $number; + } + } + + if (!\is_null($min)) { + return $min; + } + + throw $this->error('At least one argument must be passed.'); + } + + protected static $libMax = ['numbers...']; + protected function libMax($args) + { + /** + * @var Number|null + */ + $max = null; + + foreach ($args[0][2] as $arg) { + $number = $this->assertNumber($arg); + + if (\is_null($max) || $max->lessThan($number)) { + $max = $number; + } + } + + if (!\is_null($max)) { + return $max; + } + + throw $this->error('At least one argument must be passed.'); + } + + protected static $libLength = ['list']; + protected function libLength($args) + { + $list = $this->coerceList($args[0], ',', true); + + return new Number(\count($list[2]), ''); + } + + protected static $libListSeparator = ['list']; + protected function libListSeparator($args) + { + if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { + return [Type::T_KEYWORD, 'space']; + } + + $list = $this->coerceList($args[0]); + + if ($list[1] === '' && \count($list[2]) <= 1 && empty($list['enclosing'])) { + return [Type::T_KEYWORD, 'space']; + } + + if ($list[1] === ',') { + return [Type::T_KEYWORD, 'comma']; + } + + if ($list[1] === '/') { + return [Type::T_KEYWORD, 'slash']; + } + + return [Type::T_KEYWORD, 'space']; + } + + protected static $libNth = ['list', 'n']; + protected function libNth($args) + { + $list = $this->coerceList($args[0], ',', false); + $n = $this->assertInteger($args[1]); + + if ($n > 0) { + $n--; + } elseif ($n < 0) { + $n += \count($list[2]); + } + + return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue; + } + + protected static $libSetNth = ['list', 'n', 'value']; + protected function libSetNth($args) + { + $list = $this->coerceList($args[0]); + $n = $this->assertInteger($args[1]); + + if ($n > 0) { + $n--; + } elseif ($n < 0) { + $n += \count($list[2]); + } + + if (! isset($list[2][$n])) { + throw $this->error('Invalid argument for "n"'); + } + + $list[2][$n] = $args[2]; + + return $list; + } + + protected static $libMapGet = ['map', 'key', 'keys...']; + protected function libMapGet($args) + { + $map = $this->assertMap($args[0], 'map'); + if (!isset($args[2])) { + // BC layer for usages of the function from PHP code rather than from the Sass function + $args[2] = self::$emptyArgumentList; + } + $keys = array_merge([$args[1]], $args[2][2]); + $value = static::$null; + + foreach ($keys as $key) { + if (!\is_array($map) || $map[0] !== Type::T_MAP) { + return static::$null; + } + + $map = $this->mapGet($map, $key); + + if ($map === null) { + return static::$null; + } + + $value = $map; + } + + return $value; + } + + /** + * Gets the value corresponding to that key in the map + * + * @param array $map + * @param Number|array $key + * + * @return Number|array|null + */ + private function mapGet(array $map, $key) + { + $index = $this->mapGetEntryIndex($map, $key); + + if ($index !== null) { + return $map[2][$index]; + } + + return null; + } + + /** + * Gets the index corresponding to that key in the map entries + * + * @param array $map + * @param Number|array $key + * + * @return int|null + */ + private function mapGetEntryIndex(array $map, $key) + { + $key = $this->compileStringContent($this->coerceString($key)); + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return $i; + } + } + + return null; + } + + protected static $libMapKeys = ['map']; + protected function libMapKeys($args) + { + $map = $this->assertMap($args[0], 'map'); + $keys = $map[1]; + + return [Type::T_LIST, ',', $keys]; + } + + protected static $libMapValues = ['map']; + protected function libMapValues($args) + { + $map = $this->assertMap($args[0], 'map'); + $values = $map[2]; + + return [Type::T_LIST, ',', $values]; + } + + protected static $libMapRemove = [ + ['map'], + ['map', 'key', 'keys...'], + ]; + protected function libMapRemove($args) + { + $map = $this->assertMap($args[0], 'map'); + + if (\count($args) === 1) { + return $map; + } + + $keys = []; + $keys[] = $this->compileStringContent($this->coerceString($args[1])); + + foreach ($args[2][2] as $key) { + $keys[] = $this->compileStringContent($this->coerceString($key)); + } + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) { + array_splice($map[1], $i, 1); + array_splice($map[2], $i, 1); + } + } + + return $map; + } + + protected static $libMapHasKey = ['map', 'key', 'keys...']; + protected function libMapHasKey($args) + { + $map = $this->assertMap($args[0], 'map'); + if (!isset($args[2])) { + // BC layer for usages of the function from PHP code rather than from the Sass function + $args[2] = self::$emptyArgumentList; + } + $keys = array_merge([$args[1]], $args[2][2]); + $lastKey = array_pop($keys); + + foreach ($keys as $key) { + $value = $this->mapGet($map, $key); + + if ($value === null || $value instanceof Number || $value[0] !== Type::T_MAP) { + return self::$false; + } + + $map = $value; + } + + return $this->toBool($this->mapHasKey($map, $lastKey)); + } + + /** + * @param array|Number $keyValue + * + * @return bool + */ + private function mapHasKey(array $map, $keyValue) + { + $key = $this->compileStringContent($this->coerceString($keyValue)); + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return true; + } + } + + return false; + } + + protected static $libMapMerge = [ + ['map1', 'map2'], + ['map-1', 'map-2'], + ['map1', 'args...'] + ]; + protected function libMapMerge($args) + { + $map1 = $this->assertMap($args[0], 'map1'); + $map2 = $args[1]; + $keys = []; + if ($map2[0] === Type::T_LIST && isset($map2[3]) && \is_array($map2[3])) { + // This is an argument list for the variadic signature + if (\count($map2[2]) === 0) { + throw new SassScriptException('Expected $args to contain a key.'); + } + if (\count($map2[2]) === 1) { + throw new SassScriptException('Expected $args to contain a value.'); + } + $keys = $map2[2]; + $map2 = array_pop($keys); + } + $map2 = $this->assertMap($map2, 'map2'); + + return $this->modifyMap($map1, $keys, function ($oldValue) use ($map2) { + $nestedMap = $this->tryMap($oldValue); + + if ($nestedMap === null) { + return $map2; + } + + return $this->mergeMaps($nestedMap, $map2); + }); + } + + /** + * @param array $map + * @param array $keys + * @param callable $modify + * @param bool $addNesting + * + * @return Number|array + * + * @phpstan-param array $keys + * @phpstan-param callable(Number|array): (Number|array) $modify + */ + private function modifyMap(array $map, array $keys, callable $modify, $addNesting = true) + { + if ($keys === []) { + return $modify($map); + } + + return $this->modifyNestedMap($map, $keys, $modify, $addNesting); + } + + /** + * @param array $map + * @param array $keys + * @param callable $modify + * @param bool $addNesting + * + * @return array + * + * @phpstan-param non-empty-array $keys + * @phpstan-param callable(Number|array): (Number|array) $modify + */ + private function modifyNestedMap(array $map, array $keys, callable $modify, $addNesting) + { + $key = array_shift($keys); + + $nestedValueIndex = $this->mapGetEntryIndex($map, $key); + + if ($keys === []) { + if ($nestedValueIndex !== null) { + $map[2][$nestedValueIndex] = $modify($map[2][$nestedValueIndex]); + } else { + $map[1][] = $key; + $map[2][] = $modify(self::$null); + } + + return $map; + } + + $nestedMap = $nestedValueIndex !== null ? $this->tryMap($map[2][$nestedValueIndex]) : null; + + if ($nestedMap === null && !$addNesting) { + return $map; + } + + if ($nestedMap === null) { + $nestedMap = self::$emptyMap; + } + + $newNestedMap = $this->modifyNestedMap($nestedMap, $keys, $modify, $addNesting); + + if ($nestedValueIndex !== null) { + $map[2][$nestedValueIndex] = $newNestedMap; + } else { + $map[1][] = $key; + $map[2][] = $newNestedMap; + } + + return $map; + } + + /** + * Merges 2 Sass maps together + * + * @param array $map1 + * @param array $map2 + * + * @return array + */ + private function mergeMaps(array $map1, array $map2) + { + foreach ($map2[1] as $i2 => $key2) { + $map1EntryIndex = $this->mapGetEntryIndex($map1, $key2); + + if ($map1EntryIndex !== null) { + $map1[2][$map1EntryIndex] = $map2[2][$i2]; + continue; + } + + $map1[1][] = $key2; + $map1[2][] = $map2[2][$i2]; + } + + return $map1; + } + + protected static $libKeywords = ['args']; + protected function libKeywords($args) + { + $value = $args[0]; + + if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { + $compiledValue = $this->compileValue($value); + + throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args'); + } + + $keys = []; + $values = []; + + foreach ($this->getArgumentListKeywords($value) as $name => $arg) { + $keys[] = [Type::T_KEYWORD, $name]; + $values[] = $arg; + } + + return [Type::T_MAP, $keys, $values]; + } + + protected static $libIsBracketed = ['list']; + protected function libIsBracketed($args) + { + $list = $args[0]; + $this->coerceList($list, ' '); + + if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { + return self::$true; + } + + return self::$false; + } + + /** + * @param array $list1 + * @param array|Number|null $sep + * + * @return string + * @throws CompilerException + * + * @deprecated + */ + protected function listSeparatorForJoin($list1, $sep) + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + if (! isset($sep)) { + return $list1[1]; + } + + switch ($this->compileValue($sep)) { + case 'comma': + return ','; + + case 'space': + return ' '; + + default: + return $list1[1]; + } + } + + protected static $libJoin = ['list1', 'list2', 'separator:auto', 'bracketed:auto']; + protected function libJoin($args) + { + list($list1, $list2, $sep, $bracketed) = $args; + + $list1 = $this->coerceList($list1, ' ', true); + $list2 = $this->coerceList($list2, ' ', true); + + switch ($this->compileStringContent($this->assertString($sep, 'separator'))) { + case 'comma': + $separator = ','; + break; + + case 'space': + $separator = ' '; + break; + + case 'slash': + $separator = '/'; + break; + + case 'auto': + if ($list1[1] !== '' || count($list1[2]) > 1 || !empty($list1['enclosing']) && $list1['enclosing'] !== 'parent') { + $separator = $list1[1] ?: ' '; + } elseif ($list2[1] !== '' || count($list2[2]) > 1 || !empty($list2['enclosing']) && $list2['enclosing'] !== 'parent') { + $separator = $list2[1] ?: ' '; + } else { + $separator = ' '; + } + break; + + default: + throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator'); + } + + if ($bracketed === static::$true) { + $bracketed = true; + } elseif ($bracketed === static::$false) { + $bracketed = false; + } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) { + $bracketed = 'auto'; + } elseif ($bracketed === static::$null) { + $bracketed = false; + } else { + $bracketed = $this->compileValue($bracketed); + $bracketed = ! ! $bracketed; + + if ($bracketed === true) { + $bracketed = true; + } + } + + if ($bracketed === 'auto') { + $bracketed = false; + + if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') { + $bracketed = true; + } + } + + $res = [Type::T_LIST, $separator, array_merge($list1[2], $list2[2])]; + + if ($bracketed) { + $res['enclosing'] = 'bracket'; + } + + return $res; + } + + protected static $libAppend = ['list', 'val', 'separator:auto']; + protected function libAppend($args) + { + list($list1, $value, $sep) = $args; + + $list1 = $this->coerceList($list1, ' ', true); + + switch ($this->compileStringContent($this->assertString($sep, 'separator'))) { + case 'comma': + $separator = ','; + break; + + case 'space': + $separator = ' '; + break; + + case 'slash': + $separator = '/'; + break; + + case 'auto': + $separator = $list1[1] === '' && \count($list1[2]) <= 1 && (empty($list1['enclosing']) || $list1['enclosing'] === 'parent') ? ' ' : $list1[1]; + break; + + default: + throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator'); + } + + $res = [Type::T_LIST, $separator, array_merge($list1[2], [$value])]; + + if (isset($list1['enclosing'])) { + $res['enclosing'] = $list1['enclosing']; + } + + return $res; + } + + protected static $libZip = ['lists...']; + protected function libZip($args) + { + $argLists = []; + foreach ($args[0][2] as $arg) { + $argLists[] = $this->coerceList($arg); + } + + $lists = []; + $firstList = array_shift($argLists); + + $result = [Type::T_LIST, ',', $lists]; + if (! \is_null($firstList)) { + foreach ($firstList[2] as $key => $item) { + $list = [Type::T_LIST, ' ', [$item]]; + + foreach ($argLists as $arg) { + if (isset($arg[2][$key])) { + $list[2][] = $arg[2][$key]; + } else { + break 2; + } + } + + $lists[] = $list; + } + + $result[2] = $lists; + } else { + $result['enclosing'] = 'parent'; + } + + return $result; + } + + protected static $libTypeOf = ['value']; + protected function libTypeOf($args) + { + $value = $args[0]; + + return [Type::T_KEYWORD, $this->getTypeOf($value)]; + } + + /** + * @param array|Number $value + * + * @return string + */ + private function getTypeOf($value) + { + switch ($value[0]) { + case Type::T_KEYWORD: + if ($value === static::$true || $value === static::$false) { + return 'bool'; + } + + if ($this->coerceColor($value)) { + return 'color'; + } + + // fall-thru + case Type::T_FUNCTION: + return 'string'; + + case Type::T_FUNCTION_REFERENCE: + return 'function'; + + case Type::T_LIST: + if (isset($value[3]) && \is_array($value[3])) { + return 'arglist'; + } + + // fall-thru + default: + return $value[0]; + } + } + + protected static $libUnit = ['number']; + protected function libUnit($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return [Type::T_STRING, '"', [$num->unitStr()]]; + } + + protected static $libUnitless = ['number']; + protected function libUnitless($args) + { + $value = $this->assertNumber($args[0], 'number'); + + return $this->toBool($value->unitless()); + } + + protected static $libComparable = [ + ['number1', 'number2'], + ['number-1', 'number-2'] + ]; + protected function libComparable($args) + { + list($number1, $number2) = $args; + + if ( + ! $number1 instanceof Number || + ! $number2 instanceof Number + ) { + throw $this->error('Invalid argument(s) for "comparable"'); + } + + return $this->toBool($number1->isComparableTo($number2)); + } + + protected static $libStrIndex = ['string', 'substring']; + protected function libStrIndex($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $substring = $this->assertString($args[1], 'substring'); + $substringContent = $this->compileStringContent($substring); + + if (! \strlen($substringContent)) { + $result = 0; + } else { + $result = Util::mbStrpos($stringContent, $substringContent); + } + + return $result === false ? static::$null : new Number($result + 1, ''); + } + + protected static $libStrInsert = ['string', 'insert', 'index']; + protected function libStrInsert($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $insert = $this->assertString($args[1], 'insert'); + $insertContent = $this->compileStringContent($insert); + + $index = $this->assertInteger($args[2], 'index'); + if ($index > 0) { + $index = $index - 1; + } + if ($index < 0) { + $index = max(Util::mbStrlen($stringContent) + 1 + $index, 0); + } + + $string[2] = [ + Util::mbSubstr($stringContent, 0, $index), + $insertContent, + Util::mbSubstr($stringContent, $index) + ]; + + return $string; + } + + protected static $libStrLength = ['string']; + protected function libStrLength($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + return new Number(Util::mbStrlen($stringContent), ''); + } + + protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; + protected function libStrSlice($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $start = $this->assertNumber($args[1], 'start-at'); + $start->assertNoUnits('start-at'); + $startInt = $this->assertInteger($start, 'start-at'); + $end = $this->assertNumber($args[2], 'end-at'); + $end->assertNoUnits('end-at'); + $endInt = $this->assertInteger($end, 'end-at'); + + if ($endInt === 0) { + return [Type::T_STRING, $string[1], []]; + } + + if ($startInt > 0) { + $startInt--; + } + + if ($endInt < 0) { + $endInt = Util::mbStrlen($stringContent) + $endInt; + } else { + $endInt--; + } + + if ($endInt < $startInt) { + return [Type::T_STRING, $string[1], []]; + } + + $length = $endInt - $startInt + 1; // The end of the slice is inclusive + + $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)]; + + return $string; + } + + protected static $libToLowerCase = ['string']; + protected function libToLowerCase($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')]; + + return $string; + } + + protected static $libToUpperCase = ['string']; + protected function libToUpperCase($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')]; + + return $string; + } + + /** + * Apply a filter on a string content, only on ascii chars + * let extended chars untouched + * + * @param string $stringContent + * @param callable $filter + * @return string + */ + protected function stringTransformAsciiOnly($stringContent, $filter) + { + $mblength = Util::mbStrlen($stringContent); + if ($mblength === strlen($stringContent)) { + return $filter($stringContent); + } + $filteredString = ""; + for ($i = 0; $i < $mblength; $i++) { + $char = Util::mbSubstr($stringContent, $i, 1); + if (strlen($char) > 1) { + $filteredString .= $char; + } else { + $filteredString .= $filter($char); + } + } + + return $filteredString; + } + + protected static $libFeatureExists = ['feature']; + protected function libFeatureExists($args) + { + $string = $this->assertString($args[0], 'feature'); + $name = $this->compileStringContent($string); + + return $this->toBool( + \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false + ); + } + + protected static $libFunctionExists = ['name']; + protected function libFunctionExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + // user defined functions + if ($this->has(static::$namespaces['function'] . $name)) { + return self::$true; + } + + $name = $this->normalizeName($name); + + if (isset($this->userFunctions[$name])) { + return self::$true; + } + + // built-in functions + $f = $this->getBuiltinFunction($name); + + return $this->toBool(\is_callable($f)); + } + + protected static $libGlobalVariableExists = ['name']; + protected function libGlobalVariableExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + return $this->toBool($this->has($name, $this->rootEnv)); + } + + protected static $libMixinExists = ['name']; + protected function libMixinExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + return $this->toBool($this->has(static::$namespaces['mixin'] . $name)); + } + + protected static $libVariableExists = ['name']; + protected function libVariableExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + return $this->toBool($this->has($name)); + } + + protected static $libCounter = ['args...']; + /** + * Workaround IE7's content counter bug. + * + * @param array $args + * + * @return array + */ + protected function libCounter($args) + { + $list = array_map([$this, 'compileValue'], $args[0][2]); + + return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; + } + + protected static $libRandom = ['limit:null']; + protected function libRandom($args) + { + if (isset($args[0]) && $args[0] !== static::$null) { + $limit = $this->assertNumber($args[0], 'limit'); + + if ($limit->hasUnits()) { + $unitString = $limit->unitStr(); + $message = <<addLocationToMessage($message)); + } + + $n = $this->assertInteger($limit, 'limit'); + + if ($n < 1) { + throw new SassScriptException("\$limit: Must be greater than 0, was $n."); + } + + return new Number(mt_rand(1, $n), ''); + } + + $max = mt_getrandmax(); + return new Number(mt_rand(0, $max - 1) / $max, ''); + } + + protected static $libUniqueId = []; + protected function libUniqueId() + { + static $id; + + if (! isset($id)) { + $id = PHP_INT_SIZE === 4 + ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT) + : mt_rand(0, pow(36, 8)); + } + + $id += mt_rand(0, 10) + 1; + + return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; + } + + /** + * @param array|Number $value + * @param bool $force_enclosing_display + * + * @return array + */ + protected function inspectFormatValue($value, $force_enclosing_display = false) + { + if ($value === static::$null) { + $value = [Type::T_KEYWORD, 'null']; + } + + $stringValue = [$value]; + + if ($value instanceof Number) { + return [Type::T_STRING, '', $stringValue]; + } + + if ($value[0] === Type::T_LIST) { + if (end($value[2]) === static::$null) { + array_pop($value[2]); + $value[2][] = [Type::T_STRING, '', ['']]; + $force_enclosing_display = true; + } + + if ( + ! empty($value['enclosing']) && + ($force_enclosing_display || + ($value['enclosing'] === 'bracket') || + ! \count($value[2])) + ) { + $value['enclosing'] = 'forced_' . $value['enclosing']; + $force_enclosing_display = true; + } elseif (! \count($value[2])) { + $value['enclosing'] = 'forced_parent'; + } + + foreach ($value[2] as $k => $listelement) { + $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display); + } + + $stringValue = [$value]; + } + + return [Type::T_STRING, '', $stringValue]; + } + + protected static $libInspect = ['value']; + protected function libInspect($args) + { + $value = $args[0]; + + return $this->inspectFormatValue($value); + } + + /** + * Preprocess selector args + * + * @param array $arg + * @param string|null $varname + * @param bool $allowParent + * + * @return array + */ + protected function getSelectorArg($arg, $varname = null, $allowParent = false) + { + static $parser = null; + + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + if (! $this->checkSelectorArgType($arg)) { + $var_value = $this->compileValue($arg); + throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname); + } + + + if ($arg[0] === Type::T_STRING) { + $arg[1] = ''; + } + $arg = $this->compileValue($arg); + + $parsedSelector = []; + + if ($parser->parseSelector($arg, $parsedSelector, true)) { + $selector = $this->evalSelectors($parsedSelector); + $gluedSelector = $this->glueFunctionSelectors($selector); + + if (! $allowParent) { + foreach ($gluedSelector as $selector) { + foreach ($selector as $s) { + if (in_array(static::$selfSelector, $s)) { + throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname); + } + } + } + } + + return $gluedSelector; + } + + throw SassScriptException::forArgument("expected more input, invalid selector.", $varname); + } + + /** + * Check variable type for getSelectorArg() function + * @param array $arg + * @param int $maxDepth + * @return bool + */ + protected function checkSelectorArgType($arg, $maxDepth = 2) + { + if ($arg[0] === Type::T_LIST && $maxDepth > 0) { + foreach ($arg[2] as $elt) { + if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) { + return false; + } + } + return true; + } + if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) { + return false; + } + return true; + } + + /** + * Postprocess selector to output in right format + * + * @param array $selectors + * + * @return array + */ + protected function formatOutputSelector($selectors) + { + $selectors = $this->collapseSelectorsAsList($selectors); + + return $selectors; + } + + protected static $libIsSuperselector = ['super', 'sub']; + protected function libIsSuperselector($args) + { + list($super, $sub) = $args; + + $super = $this->getSelectorArg($super, 'super'); + $sub = $this->getSelectorArg($sub, 'sub'); + + return $this->toBool($this->isSuperSelector($super, $sub)); + } + + /** + * Test a $super selector again $sub + * + * @param array $super + * @param array $sub + * + * @return bool + */ + protected function isSuperSelector($super, $sub) + { + // one and only one selector for each arg + if (! $super) { + throw $this->error('Invalid super selector for isSuperSelector()'); + } + + if (! $sub) { + throw $this->error('Invalid sub selector for isSuperSelector()'); + } + + if (count($sub) > 1) { + foreach ($sub as $s) { + if (! $this->isSuperSelector($super, [$s])) { + return false; + } + } + return true; + } + + if (count($super) > 1) { + foreach ($super as $s) { + if ($this->isSuperSelector([$s], $sub)) { + return true; + } + } + return false; + } + + $super = reset($super); + $sub = reset($sub); + + $i = 0; + $nextMustMatch = false; + + foreach ($super as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + if ($this->isImmediateRelationshipCombinator($compound)) { + if ($node !== $sub[$i]) { + return false; + } + + $nextMustMatch = true; + $i++; + } else { + while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) { + if ($nextMustMatch) { + return false; + } + + $i++; + } + + if ($i >= \count($sub)) { + return false; + } + + $nextMustMatch = false; + $i++; + } + } + + return true; + } + + /** + * Test a part of super selector again a part of sub selector + * + * @param array $superParts + * @param array $subParts + * + * @return bool + */ + protected function isSuperPart($superParts, $subParts) + { + $i = 0; + + foreach ($superParts as $superPart) { + while ($i < \count($subParts) && $subParts[$i] !== $superPart) { + $i++; + } + + if ($i >= \count($subParts)) { + return false; + } + + $i++; + } + + return true; + } + + protected static $libSelectorAppend = ['selector...']; + protected function libSelectorAppend($args) + { + // get the selector... list + $args = reset($args); + $args = $args[2]; + + if (\count($args) < 1) { + throw $this->error('selector-append() needs at least 1 argument'); + } + + $selectors = []; + foreach ($args as $arg) { + $selectors[] = $this->getSelectorArg($arg, 'selector'); + } + + return $this->formatOutputSelector($this->selectorAppend($selectors)); + } + + /** + * Append parts of the last selector in the list to the previous, recursively + * + * @param array $selectors + * + * @return array + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException + */ + protected function selectorAppend($selectors) + { + $lastSelectors = array_pop($selectors); + + if (! $lastSelectors) { + throw $this->error('Invalid selector list in selector-append()'); + } + + while (\count($selectors)) { + $previousSelectors = array_pop($selectors); + + if (! $previousSelectors) { + throw $this->error('Invalid selector list in selector-append()'); + } + + // do the trick, happening $lastSelector to $previousSelector + $appended = []; + + foreach ($previousSelectors as $previousSelector) { + foreach ($lastSelectors as $lastSelector) { + $previous = $previousSelector; + foreach ($previousSelector as $j => $previousSelectorParts) { + foreach ($lastSelector as $lastSelectorParts) { + foreach ($lastSelectorParts as $lastSelectorPart) { + $previous[$j][] = $lastSelectorPart; + } + } + } + + $appended[] = $previous; + } + } + + $lastSelectors = $appended; + } + + return $lastSelectors; + } + + protected static $libSelectorExtend = [ + ['selector', 'extendee', 'extender'], + ['selectors', 'extendee', 'extender'] + ]; + protected function libSelectorExtend($args) + { + list($selectors, $extendee, $extender) = $args; + + $selectors = $this->getSelectorArg($selectors, 'selector'); + $extendee = $this->getSelectorArg($extendee, 'extendee'); + $extender = $this->getSelectorArg($extender, 'extender'); + + if (! $selectors || ! $extendee || ! $extender) { + throw $this->error('selector-extend() invalid arguments'); + } + + $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); + + return $this->formatOutputSelector($extended); + } + + protected static $libSelectorReplace = [ + ['selector', 'original', 'replacement'], + ['selectors', 'original', 'replacement'] + ]; + protected function libSelectorReplace($args) + { + list($selectors, $original, $replacement) = $args; + + $selectors = $this->getSelectorArg($selectors, 'selector'); + $original = $this->getSelectorArg($original, 'original'); + $replacement = $this->getSelectorArg($replacement, 'replacement'); + + if (! $selectors || ! $original || ! $replacement) { + throw $this->error('selector-replace() invalid arguments'); + } + + $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true); + + return $this->formatOutputSelector($replaced); + } + + /** + * Extend/replace in selectors + * used by selector-extend and selector-replace that use the same logic + * + * @param array $selectors + * @param array $extendee + * @param array $extender + * @param bool $replace + * + * @return array + */ + protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false) + { + $saveExtends = $this->extends; + $saveExtendsMap = $this->extendsMap; + + $this->extends = []; + $this->extendsMap = []; + + foreach ($extendee as $es) { + if (\count($es) !== 1) { + throw $this->error('Can\'t extend complex selector.'); + } + + // only use the first one + $this->pushExtends(reset($es), $extender, null); + } + + $extended = []; + + foreach ($selectors as $selector) { + if (! $replace) { + $extended[] = $selector; + } + + $n = \count($extended); + + $this->matchExtends($selector, $extended); + + // if didnt match, keep the original selector if we are in a replace operation + if ($replace && \count($extended) === $n) { + $extended[] = $selector; + } + } + + $this->extends = $saveExtends; + $this->extendsMap = $saveExtendsMap; + + return $extended; + } + + protected static $libSelectorNest = ['selector...']; + protected function libSelectorNest($args) + { + // get the selector... list + $args = reset($args); + $args = $args[2]; + + if (\count($args) < 1) { + throw $this->error('selector-nest() needs at least 1 argument'); + } + + $selectorsMap = []; + foreach ($args as $arg) { + $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true); + } + + assert(!empty($selectorsMap)); + + $envs = []; + + foreach ($selectorsMap as $selectors) { + $env = new Environment(); + $env->selectors = $selectors; + + $envs[] = $env; + } + + $envs = array_reverse($envs); + $env = $this->extractEnv($envs); + $outputSelectors = $this->multiplySelectors($env); + + return $this->formatOutputSelector($outputSelectors); + } + + protected static $libSelectorParse = [ + ['selector'], + ['selectors'] + ]; + protected function libSelectorParse($args) + { + $selectors = reset($args); + $selectors = $this->getSelectorArg($selectors, 'selector'); + + return $this->formatOutputSelector($selectors); + } + + protected static $libSelectorUnify = ['selectors1', 'selectors2']; + protected function libSelectorUnify($args) + { + list($selectors1, $selectors2) = $args; + + $selectors1 = $this->getSelectorArg($selectors1, 'selectors1'); + $selectors2 = $this->getSelectorArg($selectors2, 'selectors2'); + + if (! $selectors1 || ! $selectors2) { + throw $this->error('selector-unify() invalid arguments'); + } + + // only consider the first compound of each + $compound1 = reset($selectors1); + $compound2 = reset($selectors2); + + // unify them and that's it + $unified = $this->unifyCompoundSelectors($compound1, $compound2); + + return $this->formatOutputSelector($unified); + } + + /** + * The selector-unify magic as its best + * (at least works as expected on test cases) + * + * @param array $compound1 + * @param array $compound2 + * + * @return array + */ + protected function unifyCompoundSelectors($compound1, $compound2) + { + if (! \count($compound1)) { + return $compound2; + } + + if (! \count($compound2)) { + return $compound1; + } + + // check that last part are compatible + $lastPart1 = array_pop($compound1); + $lastPart2 = array_pop($compound2); + $last = $this->mergeParts($lastPart1, $lastPart2); + + if (! $last) { + return [[]]; + } + + $unifiedCompound = [$last]; + $unifiedSelectors = [$unifiedCompound]; + + // do the rest + while (\count($compound1) || \count($compound2)) { + $part1 = end($compound1); + $part2 = end($compound2); + + if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) { + list($compound2, $part2, $after2) = $match2; + + if ($after2) { + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2); + } + + $c = $this->mergeParts($part1, $part2); + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + + $part1 = $part2 = null; + + array_pop($compound1); + } + + if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) { + list($compound1, $part1, $after1) = $match1; + + if ($after1) { + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1); + } + + $c = $this->mergeParts($part2, $part1); + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + + $part1 = $part2 = null; + + array_pop($compound2); + } + + $new = []; + + if ($part1 && $part2) { + array_pop($compound1); + array_pop($compound2); + + $s = $this->prependSelectors($unifiedSelectors, [$part2]); + $new = array_merge($new, $this->prependSelectors($s, [$part1])); + $s = $this->prependSelectors($unifiedSelectors, [$part1]); + $new = array_merge($new, $this->prependSelectors($s, [$part2])); + } elseif ($part1) { + array_pop($compound1); + + $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1])); + } elseif ($part2) { + array_pop($compound2); + + $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2])); + } + + if ($new) { + $unifiedSelectors = $new; + } + } + + return $unifiedSelectors; + } + + /** + * Prepend each selector from $selectors with $parts + * + * @param array $selectors + * @param array $parts + * + * @return array + */ + protected function prependSelectors($selectors, $parts) + { + $new = []; + + foreach ($selectors as $compoundSelector) { + array_unshift($compoundSelector, $parts); + + $new[] = $compoundSelector; + } + + return $new; + } + + /** + * Try to find a matching part in a compound: + * - with same html tag name + * - with some class or id or something in common + * + * @param array $part + * @param array $compound + * + * @return array|false + */ + protected function matchPartInCompound($part, $compound) + { + $partTag = $this->findTagName($part); + $before = $compound; + $after = []; + + // try to find a match by tag name first + while (\count($before)) { + $p = array_pop($before); + + if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) { + return [$before, $p, $after]; + } + + $after[] = $p; + } + + // try again matching a non empty intersection and a compatible tagname + $before = $compound; + $after = []; + + while (\count($before)) { + $p = array_pop($before); + + if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) { + if (\count(array_intersect($part, $p))) { + return [$before, $p, $after]; + } + } + + $after[] = $p; + } + + return false; + } + + /** + * Merge two part list taking care that + * - the html tag is coming first - if any + * - the :something are coming last + * + * @param array $parts1 + * @param array $parts2 + * + * @return array + */ + protected function mergeParts($parts1, $parts2) + { + $tag1 = $this->findTagName($parts1); + $tag2 = $this->findTagName($parts2); + $tag = $this->checkCompatibleTags($tag1, $tag2); + + // not compatible tags + if ($tag === false) { + return []; + } + + if ($tag) { + if ($tag1) { + $parts1 = array_diff($parts1, [$tag1]); + } + + if ($tag2) { + $parts2 = array_diff($parts2, [$tag2]); + } + } + + $mergedParts = array_merge($parts1, $parts2); + $mergedOrderedParts = []; + + foreach ($mergedParts as $part) { + if (strpos($part, ':') === 0) { + $mergedOrderedParts[] = $part; + } + } + + $mergedParts = array_diff($mergedParts, $mergedOrderedParts); + $mergedParts = array_merge($mergedParts, $mergedOrderedParts); + + if ($tag) { + array_unshift($mergedParts, $tag); + } + + return $mergedParts; + } + + /** + * Check the compatibility between two tag names: + * if both are defined they should be identical or one has to be '*' + * + * @param string $tag1 + * @param string $tag2 + * + * @return array|false + */ + protected function checkCompatibleTags($tag1, $tag2) + { + $tags = [$tag1, $tag2]; + $tags = array_unique($tags); + $tags = array_filter($tags); + + if (\count($tags) > 1) { + $tags = array_diff($tags, ['*']); + } + + // not compatible nodes + if (\count($tags) > 1) { + return false; + } + + return $tags; + } + + /** + * Find the html tag name in a selector parts list + * + * @param string[] $parts + * + * @return string + */ + protected function findTagName($parts) + { + foreach ($parts as $part) { + if (! preg_match('/^[\[.:#%_-]/', $part)) { + return $part; + } + } + + return ''; + } + + protected static $libSimpleSelectors = ['selector']; + protected function libSimpleSelectors($args) + { + $selector = reset($args); + $selector = $this->getSelectorArg($selector, 'selector'); + + // remove selectors list layer, keeping the first one + $selector = reset($selector); + + // remove parts list layer, keeping the first part + $part = reset($selector); + + $listParts = []; + + foreach ($part as $p) { + $listParts[] = [Type::T_STRING, '', [$p]]; + } + + return [Type::T_LIST, ',', $listParts]; + } + + protected static $libScssphpGlob = ['pattern']; + protected function libScssphpGlob($args) + { + @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED); + + $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true); + + $string = $this->assertString($args[0], 'pattern'); + $pattern = $this->compileStringContent($string); + $matches = glob($pattern); + $listParts = []; + + foreach ($matches as $match) { + if (! is_file($match)) { + continue; + } + + $listParts[] = [Type::T_STRING, '"', [$match]]; + } + + return [Type::T_LIST, ',', $listParts]; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php b/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php new file mode 100644 index 0000000..a662919 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php @@ -0,0 +1,77 @@ + + */ + private $parsedFiles; + + /** + * @var array + * @phpstan-var list + */ + private $resolvedImports; + + /** + * @param CompilationResult $result + * @param array $parsedFiles + * @param array $resolvedImports + * + * @phpstan-param list $resolvedImports + */ + public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports) + { + $this->result = $result; + $this->parsedFiles = $parsedFiles; + $this->resolvedImports = $resolvedImports; + } + + /** + * @return CompilationResult + */ + public function getResult() + { + return $this->result; + } + + /** + * @return array + */ + public function getParsedFiles() + { + return $this->parsedFiles; + } + + /** + * @return array + * + * @phpstan-return list + */ + public function getResolvedImports() + { + return $this->resolvedImports; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php b/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php new file mode 100644 index 0000000..b205a07 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php @@ -0,0 +1,68 @@ + + * + * @internal + */ +class Environment +{ + /** + * @var \ScssPhp\ScssPhp\Block|null + */ + public $block; + + /** + * @var \ScssPhp\ScssPhp\Compiler\Environment|null + */ + public $parent; + + /** + * @var Environment|null + */ + public $declarationScopeParent; + + /** + * @var Environment|null + */ + public $parentStore; + + /** + * @var array|null + */ + public $selectors; + + /** + * @var string|null + */ + public $marker; + + /** + * @var array + */ + public $store; + + /** + * @var array + */ + public $storeUnreduced; + + /** + * @var int + */ + public $depth; +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php b/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php new file mode 100644 index 0000000..0b00cf5 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +class CompilerException extends \Exception implements SassException +{ +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php b/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php new file mode 100644 index 0000000..f072669 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php @@ -0,0 +1,58 @@ + + * + * @internal + */ +class ParserException extends \Exception implements SassException +{ + /** + * @var array|null + * @phpstan-var array{string, int, int}|null + */ + private $sourcePosition; + + /** + * Get source position + * + * @api + * + * @return array|null + * @phpstan-return array{string, int, int}|null + */ + public function getSourcePosition() + { + return $this->sourcePosition; + } + + /** + * Set source position + * + * @api + * + * @param array $sourcePosition + * + * @return void + * + * @phpstan-param array{string, int, int} $sourcePosition + */ + public function setSourcePosition($sourcePosition) + { + $this->sourcePosition = $sourcePosition; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php b/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php new file mode 100644 index 0000000..4be4dee --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +class RangeException extends \Exception implements SassException +{ +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php b/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php new file mode 100644 index 0000000..9f62b3c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php @@ -0,0 +1,7 @@ + + * + * @deprecated The Scssphp server should define its own exception instead. + */ +class ServerException extends \Exception implements SassException +{ +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php new file mode 100644 index 0000000..6137dc6 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php @@ -0,0 +1,377 @@ + + * + * @internal + */ +abstract class Formatter +{ + /** + * @var int + */ + public $indentLevel; + + /** + * @var string + */ + public $indentChar; + + /** + * @var string + */ + public $break; + + /** + * @var string + */ + public $open; + + /** + * @var string + */ + public $close; + + /** + * @var string + */ + public $tagSeparator; + + /** + * @var string + */ + public $assignSeparator; + + /** + * @var bool + */ + public $keepSemicolons; + + /** + * @var \ScssPhp\ScssPhp\Formatter\OutputBlock + */ + protected $currentBlock; + + /** + * @var int + */ + protected $currentLine; + + /** + * @var int + */ + protected $currentColumn; + + /** + * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null + */ + protected $sourceMapGenerator; + + /** + * @var string + */ + protected $strippedSemicolon; + + /** + * Initialize formatter + * + * @api + */ + abstract public function __construct(); + + /** + * Return indentation (whitespace) + * + * @return string + */ + protected function indentStr() + { + return ''; + } + + /** + * Return property assignment + * + * @api + * + * @param string $name + * @param mixed $value + * + * @return string + */ + public function property($name, $value) + { + return rtrim($name) . $this->assignSeparator . $value . ';'; + } + + /** + * Return custom property assignment + * differs in that you have to keep spaces in the value as is + * + * @api + * + * @param string $name + * @param mixed $value + * + * @return string + */ + public function customProperty($name, $value) + { + return rtrim($name) . trim($this->assignSeparator) . $value . ';'; + } + + /** + * Output lines inside a block + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + $glue = $this->break . $inner; + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write($inner + . implode($this->tagSeparator, $block->selectors) + . $this->open . $this->break); + } + + /** + * Output block children + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function blockChildren(OutputBlock $block) + { + foreach ($block->children as $child) { + $this->block($child); + } + } + + /** + * Output non-empty block + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function block(OutputBlock $block) + { + if (empty($block->lines) && empty($block->children)) { + return; + } + + $this->currentBlock = $block; + + $pre = $this->indentStr(); + + if (! empty($block->selectors)) { + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + $this->blockLines($block); + } + + if (! empty($block->children)) { + $this->blockChildren($block); + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + + if (empty($block->children)) { + $this->write($this->break); + } + + $this->write($pre . $this->close . $this->break); + } + } + + /** + * Test and clean safely empty children + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return bool + */ + protected function testEmptyChildren($block) + { + $isEmpty = empty($block->lines); + + if ($block->children) { + foreach ($block->children as $k => &$child) { + if (! $this->testEmptyChildren($child)) { + $isEmpty = false; + continue; + } + + if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) { + $child->children = []; + $child->selectors = null; + } + } + } + + return $isEmpty; + } + + /** + * Entry point to formatting a block + * + * @api + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree + * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator + * + * @return string + */ + public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null) + { + $this->sourceMapGenerator = null; + + if ($sourceMapGenerator) { + $this->currentLine = 1; + $this->currentColumn = 0; + $this->sourceMapGenerator = $sourceMapGenerator; + } + + $this->testEmptyChildren($block); + + ob_start(); + + try { + $this->block($block); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } catch (\Throwable $e) { + ob_end_clean(); + throw $e; + } + + $out = ob_get_clean(); + assert($out !== false); + + return $out; + } + + /** + * Output content + * + * @param string $str + * + * @return void + */ + protected function write($str) + { + if (! empty($this->strippedSemicolon)) { + echo $this->strippedSemicolon; + + $this->strippedSemicolon = ''; + } + + /* + * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator + * will be striped for real before a closing, otherwise displayed unchanged starting the next write + */ + if ( + ! $this->keepSemicolons && + $str && + (strpos($str, ';') !== false) && + (substr($str, -1) === ';') + ) { + $str = substr($str, 0, -1); + + $this->strippedSemicolon = ';'; + } + + if ($this->sourceMapGenerator) { + $lines = explode("\n", $str); + $lastLine = array_pop($lines); + + foreach ($lines as $line) { + // If the written line starts is empty, adding a mapping would add it for + // a non-existent column as we are at the end of the line + if ($line !== '') { + assert($this->currentBlock->sourceLine !== null); + assert($this->currentBlock->sourceName !== null); + $this->sourceMapGenerator->addMapping( + $this->currentLine, + $this->currentColumn, + $this->currentBlock->sourceLine, + //columns from parser are off by one + $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, + $this->currentBlock->sourceName + ); + } + + $this->currentLine++; + $this->currentColumn = 0; + } + + if ($lastLine !== '') { + assert($this->currentBlock->sourceLine !== null); + assert($this->currentBlock->sourceName !== null); + $this->sourceMapGenerator->addMapping( + $this->currentLine, + $this->currentColumn, + $this->currentBlock->sourceLine, + //columns from parser are off by one + $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, + $this->currentBlock->sourceName + ); + } + + $this->currentColumn += \strlen($lastLine); + } + + echo $str; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php new file mode 100644 index 0000000..22f2268 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php @@ -0,0 +1,52 @@ + + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal + */ +class Compact extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Compact formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = ''; + $this->open = ' {'; + $this->close = "}\n\n"; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + public function indentStr() + { + return ' '; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php new file mode 100644 index 0000000..58ebe3f --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php @@ -0,0 +1,83 @@ + + * + * @internal + */ +class Compressed extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = false; + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') { + unset($block->lines[$index]); + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php new file mode 100644 index 0000000..2bc1e92 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php @@ -0,0 +1,87 @@ + + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal + */ +class Crunched extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Crunched formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = false; + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + unset($block->lines[$index]); + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php new file mode 100644 index 0000000..b3f4422 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php @@ -0,0 +1,127 @@ + + * + * @deprecated since 1.4.0. + * + * @internal + */ +class Debug extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Debug formatter is deprecated since 1.4.0.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat(' ', $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->lines)) { + $this->write("{$indent}block->lines: []\n"); + + return; + } + + foreach ($block->lines as $index => $line) { + $this->write("{$indent}block->lines[{$index}]: $line\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function blockSelectors(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->selectors)) { + $this->write("{$indent}block->selectors: []\n"); + + return; + } + + foreach ($block->selectors as $index => $selector) { + $this->write("{$indent}block->selectors[{$index}]: $selector\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function blockChildren(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->children)) { + $this->write("{$indent}block->children: []\n"); + + return; + } + + $this->indentLevel++; + + foreach ($block->children as $i => $child) { + $this->block($child); + } + + $this->indentLevel--; + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + $indent = $this->indentStr(); + + $this->write("{$indent}block->type: {$block->type}\n" . + "{$indent}block->depth: {$block->depth}\n"); + + $this->currentBlock = $block; + + $this->blockSelectors($block); + $this->blockLines($block); + $this->blockChildren($block); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php new file mode 100644 index 0000000..6eb4a0c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php @@ -0,0 +1,72 @@ + + * + * @internal + */ +class Expanded extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = '}'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat($this->indentChar, $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + assert($replacedLine !== null); + $block->lines[$index] = $replacedLine; + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (empty($block->selectors) || ! empty($block->children)) { + $this->write($this->break); + } + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php new file mode 100644 index 0000000..d5ed85c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php @@ -0,0 +1,238 @@ + + * + * @deprecated since 1.4.0. Use the Expanded formatter instead. + * + * @internal + */ +class Nested extends Formatter +{ + /** + * @var int + */ + private $depth; + + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Nested formatter is deprecated since 1.4.0. Use the Expanded formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + $n = $this->depth - 1; + + return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + assert($replacedLine !== null); + $block->lines[$index] = $replacedLine; + } + } + + $this->write($inner . implode($glue, $block->lines)); + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + static $depths; + static $downLevel; + static $closeBlock; + static $previousEmpty; + static $previousHasSelector; + + if ($block->type === 'root') { + $depths = [ 0 ]; + $downLevel = ''; + $closeBlock = ''; + $this->depth = 0; + $previousEmpty = false; + $previousHasSelector = false; + } + + $isMediaOrDirective = \in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]); + $isSupport = ($block->type === Type::T_DIRECTIVE + && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false); + + while ($block->depth < end($depths) || ($block->depth == 1 && end($depths) == 1)) { + array_pop($depths); + $this->depth--; + + if ( + ! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) && + (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector) + ) { + $downLevel = $this->break; + } + + if (empty($block->lines) && empty($block->children)) { + $previousEmpty = true; + } + } + + if (empty($block->lines) && empty($block->children)) { + return; + } + + $this->currentBlock = $block; + + if (! empty($block->lines) || (! empty($block->children) && ($this->depth < 1 || $isSupport))) { + if ($block->depth > end($depths)) { + if (! $previousEmpty || $this->depth < 1) { + $this->depth++; + + $depths[] = $block->depth; + } else { + // keep the current depth unchanged but take the block depth as a new reference for following blocks + array_pop($depths); + + $depths[] = $block->depth; + } + } + } + + $previousEmpty = ($block->type === Type::T_COMMENT); + $previousHasSelector = false; + + if (! empty($block->selectors)) { + if ($closeBlock) { + $this->write($closeBlock); + $closeBlock = ''; + } + + if ($downLevel) { + $this->write($downLevel); + $downLevel = ''; + } + + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + if ($closeBlock) { + $this->write($closeBlock); + $closeBlock = ''; + } + + if ($downLevel) { + $this->write($downLevel); + $downLevel = ''; + } + + $this->blockLines($block); + + $closeBlock = $this->break; + } + + if (! empty($block->children)) { + if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) { + array_pop($depths); + + $this->depth--; + $this->blockChildren($block); + $this->depth++; + + $depths[] = $block->depth; + } else { + $this->blockChildren($block); + } + } + + // reclear to not be spoiled by children if T_DIRECTIVE + if ($block->type === Type::T_DIRECTIVE) { + $previousHasSelector = false; + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + + $this->write($this->close); + + $closeBlock = $this->break; + + if ($this->depth > 1 && ! empty($block->children)) { + array_pop($depths); + $this->depth--; + } + + if (! $isMediaOrDirective) { + $previousHasSelector = true; + } + } + + if ($block->type === 'root') { + $this->write($this->break); + } + } + + /** + * Block has flat child + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return bool + */ + private function hasFlatChild($block) + { + foreach ($block->children as $child) { + if (empty($child->selectors)) { + return true; + } + } + + return false; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php new file mode 100644 index 0000000..2799656 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php @@ -0,0 +1,68 @@ + + * + * @internal + */ +class OutputBlock +{ + /** + * @var string|null + */ + public $type; + + /** + * @var int + */ + public $depth; + + /** + * @var array|null + */ + public $selectors; + + /** + * @var string[] + */ + public $lines; + + /** + * @var OutputBlock[] + */ + public $children; + + /** + * @var OutputBlock|null + */ + public $parent; + + /** + * @var string|null + */ + public $sourceName; + + /** + * @var int|null + */ + public $sourceLine; + + /** + * @var int|null + */ + public $sourceColumn; +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php b/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php new file mode 100644 index 0000000..7c0a2f7 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php @@ -0,0 +1,48 @@ +stream = $stream; + $this->closeOnDestruct = $closeOnDestruct; + } + + /** + * @internal + */ + public function __destruct() + { + if ($this->closeOnDestruct) { + fclose($this->stream); + } + } + + /** + * @inheritDoc + */ + public function warn($message, $deprecation = false) + { + $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: '; + + fwrite($this->stream, $prefix . $message . "\n\n"); + } + + /** + * @inheritDoc + */ + public function debug($message) + { + fwrite($this->stream, $message . "\n"); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Node.php b/plugins/admin/vendor/scssphp/scssphp/src/Node.php new file mode 100644 index 0000000..fcaf8a9 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Node.php @@ -0,0 +1,43 @@ + + * + * @internal + */ +abstract class Node +{ + /** + * @var string + */ + public $type; + + /** + * @var int + */ + public $sourceIndex; + + /** + * @var int|null + */ + public $sourceLine; + + /** + * @var int|null + */ + public $sourceColumn; +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php b/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php new file mode 100644 index 0000000..48e711f --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php @@ -0,0 +1,834 @@ + + * + * @template-implements \ArrayAccess + */ +class Number extends Node implements \ArrayAccess +{ + const PRECISION = 10; + + /** + * @var int + * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore. + */ + public static $precision = self::PRECISION; + + /** + * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ + * + * @var array + * @phpstan-var array> + */ + protected static $unitTable = [ + 'in' => [ + 'in' => 1, + 'pc' => 6, + 'pt' => 72, + 'px' => 96, + 'cm' => 2.54, + 'mm' => 25.4, + 'q' => 101.6, + ], + 'turn' => [ + 'deg' => 360, + 'grad' => 400, + 'rad' => 6.28318530717958647692528676, // 2 * M_PI + 'turn' => 1, + ], + 's' => [ + 's' => 1, + 'ms' => 1000, + ], + 'Hz' => [ + 'Hz' => 1, + 'kHz' => 0.001, + ], + 'dpi' => [ + 'dpi' => 1, + 'dpcm' => 1 / 2.54, + 'dppx' => 1 / 96, + ], + ]; + + /** + * @var int|float + */ + private $dimension; + + /** + * @var string[] + * @phpstan-var list + */ + private $numeratorUnits; + + /** + * @var string[] + * @phpstan-var list + */ + private $denominatorUnits; + + /** + * Initialize number + * + * @param int|float $dimension + * @param string[]|string $numeratorUnits + * @param string[] $denominatorUnits + * + * @phpstan-param list|string $numeratorUnits + * @phpstan-param list $denominatorUnits + */ + public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) + { + if (is_string($numeratorUnits)) { + $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : []; + } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) { + // TODO get rid of this once `$number[2]` is not used anymore + $denominatorUnits = $numeratorUnits['denominator_units']; + $numeratorUnits = $numeratorUnits['numerator_units']; + } + + $this->dimension = $dimension; + $this->numeratorUnits = $numeratorUnits; + $this->denominatorUnits = $denominatorUnits; + } + + /** + * @return float|int + */ + public function getDimension() + { + return $this->dimension; + } + + /** + * @return string[] + */ + public function getNumeratorUnits() + { + return $this->numeratorUnits; + } + + /** + * @return string[] + */ + public function getDenominatorUnits() + { + return $this->denominatorUnits; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + if ($offset === -3) { + return ! \is_null($this->sourceColumn); + } + + if ($offset === -2) { + return ! \is_null($this->sourceLine); + } + + if ( + $offset === -1 || + $offset === 0 || + $offset === 1 || + $offset === 2 + ) { + return true; + } + + return false; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + switch ($offset) { + case -3: + return $this->sourceColumn; + + case -2: + return $this->sourceLine; + + case -1: + return $this->sourceIndex; + + case 0: + return Type::T_NUMBER; + + case 1: + return $this->dimension; + + case 2: + return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits); + } + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('Number is immutable'); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new \BadMethodCallException('Number is immutable'); + } + + /** + * Returns true if the number is unitless + * + * @return bool + */ + public function unitless() + { + return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; + } + + /** + * Returns true if the number has any units + * + * @return bool + */ + public function hasUnits() + { + return !$this->unitless(); + } + + /** + * Checks whether the number has exactly this unit + * + * @param string $unit + * + * @return bool + */ + public function hasUnit($unit) + { + return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit; + } + + /** + * Returns unit(s) as the product of numerator units divided by the product of denominator units + * + * @return string + */ + public function unitStr() + { + if ($this->unitless()) { + return ''; + } + + return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); + } + + /** + * @param float|int $min + * @param float|int $max + * @param string|null $name + * + * @return float|int + * @throws SassScriptException + */ + public function valueInRange($min, $max, $name = null) + { + try { + return Util::checkRange('', new Range($min, $max), $this); + } catch (RangeException $e) { + throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name); + } + } + + /** + * @param float|int $min + * @param float|int $max + * @param string $name + * @param string $unit + * + * @return float|int + * @throws SassScriptException + * + * @internal + */ + public function valueInRangeWithUnit($min, $max, $name, $unit) + { + try { + return Util::checkRange('', new Range($min, $max), $this); + } catch (RangeException $e) { + throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name); + } + } + + /** + * @param string|null $varName + * + * @return void + */ + public function assertNoUnits($varName = null) + { + if ($this->unitless()) { + return; + } + + throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName); + } + + /** + * @param string $unit + * @param string|null $varName + * + * @return void + */ + public function assertUnit($unit, $varName = null) + { + if ($this->hasUnit($unit)) { + return; + } + + throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName); + } + + /** + * @param Number $other + * + * @return void + */ + public function assertSameUnitOrUnitless(Number $other) + { + if ($other->unitless()) { + return; + } + + if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) { + return; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($other->numeratorUnits, $other->denominatorUnits) + )); + } + + /** + * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. + * + * This does not throw an error if this number is unitless and + * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead, + * it treats all unitless numbers as convertible to and from all units without + * changing the value. + * + * @param string[] $newNumeratorUnits + * @param string[] $newDenominatorUnits + * + * @return Number + * + * @phpstan-param list $newNumeratorUnits + * @phpstan-param list $newDenominatorUnits + * + * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits + */ + public function coerce(array $newNumeratorUnits, array $newDenominatorUnits) + { + return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits); + } + + /** + * @param Number $other + * + * @return bool + */ + public function isComparableTo(Number $other) + { + if ($this->unitless() || $other->unitless()) { + return true; + } + + try { + $this->greaterThan($other); + return true; + } catch (SassScriptException $e) { + return false; + } + } + + /** + * @param Number $other + * + * @return bool + */ + public function lessThan(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 < $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function lessThanOrEqual(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 <= $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function greaterThan(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 > $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function greaterThanOrEqual(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 >= $num2; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function plus(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + return $num1 + $num2; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function minus(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + return $num1 - $num2; + }); + } + + /** + * @return Number + */ + public function unaryMinus() + { + return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits); + } + + /** + * @param Number $other + * + * @return Number + */ + public function modulo(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + if ($num2 == 0) { + return NAN; + } + + $result = fmod($num1, $num2); + + if ($result == 0) { + return 0; + } + + if ($num2 < 0 xor $num1 < 0) { + $result += $num2; + } + + return $result; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function times(Number $other) + { + return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits); + } + + /** + * @param Number $other + * + * @return Number + */ + public function dividedBy(Number $other) + { + if ($other->dimension == 0) { + if ($this->dimension == 0) { + $value = NAN; + } elseif ($this->dimension > 0) { + $value = INF; + } else { + $value = -INF; + } + } else { + $value = $this->dimension / $other->dimension; + } + + return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits); + } + + /** + * @param Number $other + * + * @return bool + */ + public function equals(Number $other) + { + // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here. + if ($this->unitless() !== $other->unitless()) { + return false; + } + + // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF + if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) { + return false; + } + + if ($this->unitless()) { + return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION); + } + + try { + return $this->coerceUnits($other, function ($num1, $num2) { + return round($num1,self::PRECISION) == round($num2, self::PRECISION); + }); + } catch (SassScriptException $e) { + return false; + } + } + + /** + * Output number + * + * @param \ScssPhp\ScssPhp\Compiler $compiler + * + * @return string + */ + public function output(Compiler $compiler = null) + { + $dimension = round($this->dimension, self::PRECISION); + + if (is_nan($dimension)) { + return 'NaN'; + } + + if ($dimension === INF) { + return 'Infinity'; + } + + if ($dimension === -INF) { + return '-Infinity'; + } + + if ($compiler) { + $unit = $this->unitStr(); + } elseif (isset($this->numeratorUnits[0])) { + $unit = $this->numeratorUnits[0]; + } else { + $unit = ''; + } + + $dimension = number_format($dimension, self::PRECISION, '.', ''); + + return rtrim(rtrim($dimension, '0'), '.') . $unit; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->output(); + } + + /** + * @param Number $other + * @param callable $operation + * + * @return Number + * + * @phpstan-param callable(int|float, int|float): (int|float) $operation + */ + private function coerceNumber(Number $other, $operation) + { + $result = $this->coerceUnits($other, $operation); + + if (!$this->unitless()) { + return new Number($result, $this->numeratorUnits, $this->denominatorUnits); + } + + return new Number($result, $other->numeratorUnits, $other->denominatorUnits); + } + + /** + * @param Number $other + * @param callable $operation + * + * @return mixed + * + * @phpstan-template T + * @phpstan-param callable(int|float, int|float): T $operation + * @phpstan-return T + */ + private function coerceUnits(Number $other, $operation) + { + if (!$this->unitless()) { + $num1 = $this->dimension; + $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits); + } else { + $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits); + $num2 = $other->dimension; + } + + return \call_user_func($operation, $num1, $num2); + } + + /** + * @param string[] $numeratorUnits + * @param string[] $denominatorUnits + * + * @return int|float + * + * @phpstan-param list $numeratorUnits + * @phpstan-param list $denominatorUnits + * + * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits + */ + private function valueInUnits(array $numeratorUnits, array $denominatorUnits) + { + if ( + $this->unitless() + || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0) + || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits) + ) { + return $this->dimension; + } + + $value = $this->dimension; + $oldNumerators = $this->numeratorUnits; + + foreach ($numeratorUnits as $newNumerator) { + foreach ($oldNumerators as $key => $oldNumerator) { + $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value *= $conversionFactor; + unset($oldNumerators[$key]); + continue 2; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + $oldDenominators = $this->denominatorUnits; + + foreach ($denominatorUnits as $newDenominator) { + foreach ($oldDenominators as $key => $oldDenominator) { + $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($oldDenominators[$key]); + continue 2; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + if (\count($oldNumerators) || \count($oldDenominators)) { + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + return $value; + } + + /** + * @param int|float $value + * @param string[] $numerators1 + * @param string[] $denominators1 + * @param string[] $numerators2 + * @param string[] $denominators2 + * + * @return Number + * + * @phpstan-param list $numerators1 + * @phpstan-param list $denominators1 + * @phpstan-param list $numerators2 + * @phpstan-param list $denominators2 + */ + private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2) + { + $newNumerators = array(); + + foreach ($numerators1 as $numerator) { + foreach ($denominators2 as $key => $denominator) { + $conversionFactor = self::getConversionFactor($numerator, $denominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($denominators2[$key]); + continue 2; + } + + $newNumerators[] = $numerator; + } + + foreach ($numerators2 as $numerator) { + foreach ($denominators1 as $key => $denominator) { + $conversionFactor = self::getConversionFactor($numerator, $denominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($denominators1[$key]); + continue 2; + } + + $newNumerators[] = $numerator; + } + + $newDenominators = array_values(array_merge($denominators1, $denominators2)); + + return new Number($value, $newNumerators, $newDenominators); + } + + /** + * Returns the number of [unit1]s per [unit2]. + * + * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. + * + * @param string $unit1 + * @param string $unit2 + * + * @return float|int|null + */ + private static function getConversionFactor($unit1, $unit2) + { + if ($unit1 === $unit2) { + return 1; + } + + foreach (static::$unitTable as $unitVariants) { + if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { + return $unitVariants[$unit1] / $unitVariants[$unit2]; + } + } + + return null; + } + + /** + * Returns unit(s) as the product of numerator units divided by the product of denominator units + * + * @param string[] $numerators + * @param string[] $denominators + * + * @phpstan-param list $numerators + * @phpstan-param list $denominators + * + * @return string + */ + private static function getUnitString(array $numerators, array $denominators) + { + if (!\count($numerators)) { + if (\count($denominators) === 0) { + return 'no units'; + } + + if (\count($denominators) === 1) { + return $denominators[0] . '^-1'; + } + + return '(' . implode('*', $denominators) . ')^-1'; + } + + return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php b/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php new file mode 100644 index 0000000..c284639 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php @@ -0,0 +1,9 @@ + + * + * @internal + */ +class Parser +{ + const SOURCE_INDEX = -1; + const SOURCE_LINE = -2; + const SOURCE_COLUMN = -3; + + /** + * @var array + */ + protected static $precedence = [ + '=' => 0, + 'or' => 1, + 'and' => 2, + '==' => 3, + '!=' => 3, + '<=' => 4, + '>=' => 4, + '<' => 4, + '>' => 4, + '+' => 5, + '-' => 5, + '*' => 6, + '/' => 6, + '%' => 6, + ]; + + /** + * @var string + */ + protected static $commentPattern; + /** + * @var string + */ + protected static $operatorPattern; + /** + * @var string + */ + protected static $whitePattern; + + /** + * @var Cache|null + */ + protected $cache; + + private $sourceName; + private $sourceIndex; + /** + * @var array + */ + private $sourcePositions; + /** + * The current offset in the buffer + * + * @var int + */ + private $count; + /** + * @var Block|null + */ + private $env; + /** + * @var bool + */ + private $inParens; + /** + * @var bool + */ + private $eatWhiteDefault; + /** + * @var bool + */ + private $discardComments; + private $allowVars; + /** + * @var string + */ + private $buffer; + private $utf8; + /** + * @var string|null + */ + private $encoding; + private $patternModifiers; + private $commentsSeen; + + private $cssOnly; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Constructor + * + * @api + * + * @param string|null $sourceName + * @param int $sourceIndex + * @param string|null $encoding + * @param Cache|null $cache + * @param bool $cssOnly + * @param LoggerInterface|null $logger + */ + public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null) + { + $this->sourceName = $sourceName ?: '(stdin)'; + $this->sourceIndex = $sourceIndex; + $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; + $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; + $this->commentsSeen = []; + $this->allowVars = true; + $this->cssOnly = $cssOnly; + $this->logger = $logger ?: new QuietLogger(); + + if (empty(static::$operatorPattern)) { + static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; + + $commentSingle = '\/\/'; + $commentMultiLeft = '\/\*'; + $commentMultiRight = '\*\/'; + + static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; + static::$whitePattern = $this->utf8 + ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' + : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; + } + + $this->cache = $cache; + } + + /** + * Get source file name + * + * @api + * + * @return string + */ + public function getSourceName() + { + return $this->sourceName; + } + + /** + * Throw parser error + * + * @api + * + * @param string $msg + * + * @phpstan-return never-return + * + * @throws ParserException + * + * @deprecated use "parseError" and throw the exception in the caller instead. + */ + public function throwParseError($msg = 'parse error') + { + @trigger_error( + 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->parseError($msg); + } + + /** + * Creates a parser error + * + * @api + * + * @param string $msg + * + * @return ParserException + */ + public function parseError($msg = 'parse error') + { + list($line, $column) = $this->getSourcePosition($this->count); + + $loc = empty($this->sourceName) + ? "line: $line, column: $column" + : "$this->sourceName on line $line, at column $column"; + + if ($this->peek('(.*?)(\n|$)', $m, $this->count)) { + $this->restoreEncoding(); + + $e = new ParserException("$msg: failed at `$m[1]` $loc"); + $e->setSourcePosition([$this->sourceName, $line, $column]); + + return $e; + } + + $this->restoreEncoding(); + + $e = new ParserException("$msg: $loc"); + $e->setSourcePosition([$this->sourceName, $line, $column]); + + return $e; + } + + /** + * Parser buffer + * + * @api + * + * @param string $buffer + * + * @return Block + */ + public function parse($buffer) + { + if ($this->cache) { + $cacheKey = $this->sourceName . ':' . md5($buffer); + $parseOptions = [ + 'utf8' => $this->utf8, + ]; + $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); + + if (! \is_null($v)) { + return $v; + } + } + + // strip BOM (byte order marker) + if (substr($buffer, 0, 3) === "\xef\xbb\xbf") { + $buffer = substr($buffer, 3); + } + + $this->buffer = rtrim($buffer, "\x00..\x1f"); + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + + $this->saveEncoding(); + $this->extractLineNumbers($buffer); + + $this->pushBlock(null); // root block + $this->whitespace(); + $this->pushBlock(null); + $this->popBlock(); + + while ($this->parseChunk()) { + ; + } + + if ($this->count !== \strlen($this->buffer)) { + throw $this->parseError(); + } + + if (! empty($this->env->parent)) { + throw $this->parseError('unclosed block'); + } + + $this->restoreEncoding(); + assert($this->env !== null); + + if ($this->cache) { + $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); + } + + return $this->env; + } + + /** + * Parse a value or value list + * + * @api + * + * @param string $buffer + * @param string|array $out + * + * @return bool + */ + public function parseValue($buffer, &$out) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + $list = $this->valueList($out); + + $this->restoreEncoding(); + + return $list; + } + + /** + * Parse a selector or selector list + * + * @api + * + * @param string $buffer + * @param string|array $out + * @param bool $shouldValidate + * + * @return bool + */ + public function parseSelector($buffer, &$out, $shouldValidate = true) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + // discard space/comments at the start + $this->discardComments = true; + $this->whitespace(); + $this->discardComments = false; + + $selector = $this->selectors($out); + + $this->restoreEncoding(); + + if ($shouldValidate && $this->count !== strlen($buffer)) { + throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`"); + } + + return $selector; + } + + /** + * Parse a media Query + * + * @api + * + * @param string $buffer + * @param array $out + * + * @return bool + */ + public function parseMediaQueryList($buffer, &$out) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + $isMediaQuery = $this->mediaQueryList($out); + + $this->restoreEncoding(); + + return $isMediaQuery; + } + + /** + * Parse a single chunk off the head of the buffer and append it to the + * current parse environment. + * + * Returns false when the buffer is empty, or when there is an error. + * + * This function is called repeatedly until the entire document is + * parsed. + * + * This parser is most similar to a recursive descent parser. Single + * functions represent discrete grammatical rules for the language, and + * they are able to capture the text that represents those rules. + * + * Consider the function Compiler::keyword(). (All parse functions are + * structured the same.) + * + * The function takes a single reference argument. When calling the + * function it will attempt to match a keyword on the head of the buffer. + * If it is successful, it will place the keyword in the referenced + * argument, advance the position in the buffer, and return true. If it + * fails then it won't advance the buffer and it will return false. + * + * All of these parse functions are powered by Compiler::match(), which behaves + * the same way, but takes a literal regular expression. Sometimes it is + * more convenient to use match instead of creating a new function. + * + * Because of the format of the functions, to parse an entire string of + * grammatical rules, you can chain them together using &&. + * + * But, if some of the rules in the chain succeed before one fails, then + * the buffer position will be left at an invalid state. In order to + * avoid this, Compiler::seek() is used to remember and set buffer positions. + * + * Before parsing a chain, use $s = $this->count to remember the current + * position into $s. Then if a chain fails, use $this->seek($s) to + * go back where we started. + * + * @return bool + */ + protected function parseChunk() + { + $s = $this->count; + + // the directives + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { + if ( + $this->literal('@at-root', 8) && + ($this->selectors($selector) || true) && + ($this->map($with) || true) && + (($this->matchChar('(') && + $this->interpolation($with) && + $this->matchChar(')')) || true) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $atRoot = new AtRootBlock(); + $this->registerPushedBlock($atRoot, $s); + $atRoot->selector = $selector; + $atRoot->with = $with; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@media', 6) && + $this->mediaQueryList($mediaQueryList) && + $this->matchChar('{', false) + ) { + $media = new MediaBlock(); + $this->registerPushedBlock($media, $s); + $media->queryList = $mediaQueryList[2]; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@mixin', 6) && + $this->keyword($mixinName) && + ($this->argumentDef($args) || true) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $mixin = new CallableBlock(Type::T_MIXIN); + $this->registerPushedBlock($mixin, $s); + $mixin->name = $mixinName; + $mixin->args = $args; + + return true; + } + + $this->seek($s); + + if ( + ($this->literal('@include', 8) && + $this->keyword($mixinName) && + ($this->matchChar('(') && + ($this->argValues($argValues) || true) && + $this->matchChar(')') || true) && + ($this->end()) || + ($this->literal('using', 5) && + $this->argumentDef($argUsing) && + ($this->end() || $this->matchChar('{') && $hasBlock = true)) || + $this->matchChar('{') && $hasBlock = true) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $child = [ + Type::T_INCLUDE, + $mixinName, + isset($argValues) ? $argValues : null, + null, + isset($argUsing) ? $argUsing : null + ]; + + if (! empty($hasBlock)) { + $include = new ContentBlock(); + $this->registerPushedBlock($include, $s); + $include->child = $child; + } else { + $this->append($child, $s); + } + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@scssphp-import-once', 20) && + $this->valueList($importPath) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + list($line, $column) = $this->getSourcePosition($s); + $file = $this->sourceName; + $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); + + $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@import', 7) && + $this->valueList($importPath) && + $importPath[0] !== Type::T_FUNCTION_CALL && + $this->end() + ) { + if ($this->cssOnly) { + $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); + $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); + return true; + } + + $this->append([Type::T_IMPORT, $importPath], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@import', 7) && + $this->url($importPath) && + $this->end() + ) { + if ($this->cssOnly) { + $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); + $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); + return true; + } + + $this->append([Type::T_IMPORT, $importPath], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@extend', 7) && + $this->selectors($selectors) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + // check for '!flag' + $optional = $this->stripOptionalFlag($selectors); + $this->append([Type::T_EXTEND, $selectors, $optional], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@function', 9) && + $this->keyword($fnName) && + $this->argumentDef($args) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $func = new CallableBlock(Type::T_FUNCTION); + $this->registerPushedBlock($func, $s); + $func->name = $fnName; + $func->args = $args; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@return', 7) && + ($this->valueList($retVal) || true) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@each', 5) && + $this->genericList($varNames, 'variable', ',', false) && + $this->literal('in', 2) && + $this->valueList($list) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $each = new EachBlock(); + $this->registerPushedBlock($each, $s); + + foreach ($varNames[2] as $varName) { + $each->vars[] = $varName[1]; + } + + $each->list = $list; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@while', 6) && + $this->expression($cond) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + while ( + $cond[0] === Type::T_LIST && + ! empty($cond['enclosing']) && + $cond['enclosing'] === 'parent' && + \count($cond[2]) == 1 + ) { + $cond = reset($cond[2]); + } + + $while = new WhileBlock(); + $this->registerPushedBlock($while, $s); + $while->cond = $cond; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@for', 4) && + $this->variable($varName) && + $this->literal('from', 4) && + $this->expression($start) && + ($this->literal('through', 7) || + ($forUntil = true && $this->literal('to', 2))) && + $this->expression($end) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $for = new ForBlock(); + $this->registerPushedBlock($for, $s); + $for->var = $varName[1]; + $for->start = $start; + $for->end = $end; + $for->until = isset($forUntil); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@if', 3) && + $this->functionCallArgumentsList($cond, false, '{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $if = new IfBlock(); + $this->registerPushedBlock($if, $s); + + while ( + $cond[0] === Type::T_LIST && + ! empty($cond['enclosing']) && + $cond['enclosing'] === 'parent' && + \count($cond[2]) == 1 + ) { + $cond = reset($cond[2]); + } + + $if->cond = $cond; + $if->cases = []; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@debug', 6) && + $this->functionCallArgumentsList($value, false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_DEBUG, $value], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@warn', 5) && + $this->functionCallArgumentsList($value, false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_WARN, $value], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@error', 6) && + $this->functionCallArgumentsList($value, false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_ERROR, $value], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@content', 8) && + ($this->end() || + $this->matchChar('(') && + $this->argValues($argContent) && + $this->matchChar(')') && + $this->end()) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s); + + return true; + } + + $this->seek($s); + + $last = $this->last(); + + if (isset($last) && $last[0] === Type::T_IF) { + list(, $if) = $last; + assert($if instanceof IfBlock); + + if ($this->literal('@else', 5)) { + if ($this->matchChar('{', false)) { + $else = new ElseBlock(); + } elseif ( + $this->literal('if', 2) && + $this->functionCallArgumentsList($cond, false, '{', false) + ) { + $else = new ElseifBlock(); + $else->cond = $cond; + } + + if (isset($else)) { + $this->registerPushedBlock($else, $s); + $if->cases[] = $else; + + return true; + } + } + + $this->seek($s); + } + + // only retain the first @charset directive encountered + if ( + $this->literal('@charset', 8) && + $this->valueList($charset) && + $this->end() + ) { + return true; + } + + $this->seek($s); + + if ( + $this->literal('@supports', 9) && + ($t1 = $this->supportsQuery($supportQuery)) && + ($t2 = $this->matchChar('{', false)) + ) { + $directive = new DirectiveBlock(); + $this->registerPushedBlock($directive, $s); + $directive->name = 'supports'; + $directive->value = $supportQuery; + + return true; + } + + $this->seek($s); + + // doesn't match built in directive, do generic one + if ( + $this->matchChar('@', false) && + $this->mixedKeyword($dirName) && + $this->directiveValue($dirValue, '{') + ) { + if (count($dirName) === 1 && is_string(reset($dirName))) { + $dirName = reset($dirName); + } else { + $dirName = [Type::T_STRING, '', $dirName]; + } + if ($dirName === 'media') { + $directive = new MediaBlock(); + } else { + $directive = new DirectiveBlock(); + $directive->name = $dirName; + } + $this->registerPushedBlock($directive, $s); + + if (isset($dirValue)) { + ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue)); + $directive->value = $dirValue; + } + + return true; + } + + $this->seek($s); + + // maybe it's a generic blockless directive + if ( + $this->matchChar('@', false) && + $this->mixedKeyword($dirName) && + ! $this->isKnownGenericDirective($dirName) && + ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false))) + ) { + if (\count($dirName) === 1 && \is_string(\reset($dirName))) { + $dirName = \reset($dirName); + } else { + $dirName = [Type::T_STRING, '', $dirName]; + } + if ( + ! empty($this->env->parent) && + $this->env->type && + ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]) + ) { + $plain = \trim(\substr($this->buffer, $s, $this->count - $s)); + throw $this->parseError( + "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block" + ); + } + // blockless directives with a blank line after keeps their blank lines after + // sass-spec compliance purpose + $s = $this->count; + $hasBlankLine = false; + if ($this->match('\s*?\n\s*\n', $out, false)) { + $hasBlankLine = true; + $this->seek($s); + } + $isNotRoot = ! empty($this->env->parent); + $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s); + $this->whitespace(); + + return true; + } + + $this->seek($s); + + return false; + } + + $inCssSelector = null; + if ($this->cssOnly) { + $inCssSelector = (! empty($this->env->parent) && + ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])); + } + // custom properties : right part is static + if (($this->customProperty($name) ) && $this->matchChar(':', false)) { + $start = $this->count; + + // but can be complex and finish with ; or } + foreach ([';','}'] as $ending) { + if ( + $this->openString($ending, $stringValue, '(', ')', false) && + $this->end() + ) { + $end = $this->count; + $value = $stringValue; + + // check if we have only a partial value due to nested [] or { } to take in account + $nestingPairs = [['[', ']'], ['{', '}']]; + + foreach ($nestingPairs as $nestingPair) { + $p = strpos($this->buffer, $nestingPair[0], $start); + + if ($p && $p < $end) { + $this->seek($start); + + if ( + $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) && + $this->end() && + $this->count > $end + ) { + $end = $this->count; + $value = $stringValue; + } + } + } + + $this->seek($end); + $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s); + + return true; + } + } + + // TODO: output an error here if nothing found according to sass spec + } + + $this->seek($s); + + // property shortcut + // captures most properties before having to parse a selector + if ( + $this->keyword($name, false) && + $this->literal(': ', 2) && + $this->valueList($value) && + $this->end() + ) { + $name = [Type::T_STRING, '', [$name]]; + $this->append([Type::T_ASSIGN, $name, $value], $s); + + return true; + } + + $this->seek($s); + + // variable assigns + if ( + $this->variable($name) && + $this->matchChar(':') && + $this->valueList($value) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + // check for '!flag' + $assignmentFlags = $this->stripAssignmentFlags($value); + $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); + + return true; + } + + $this->seek($s); + + // opening css block + if ( + $this->selectors($selectors) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); + + $this->pushBlock($selectors, $s); + + if ($this->eatWhiteDefault) { + $this->whitespace(); + $this->append(null); // collect comments at the beginning if needed + } + + return true; + } + + $this->seek($s); + + // property assign, or nested assign + if ( + $this->propertyName($name) && + $this->matchChar(':') + ) { + $foundSomething = false; + + if ($this->valueList($value)) { + if (empty($this->env->parent)) { + throw $this->parseError('expected "{"'); + } + + $this->append([Type::T_ASSIGN, $name, $value], $s); + $foundSomething = true; + } + + if ($this->matchChar('{', false)) { + ! $this->cssOnly || $this->assertPlainCssValid(false); + + $propBlock = new NestedPropertyBlock(); + $this->registerPushedBlock($propBlock, $s); + $propBlock->prefix = $name; + $propBlock->hasValue = $foundSomething; + + $foundSomething = true; + } elseif ($foundSomething) { + $foundSomething = $this->end(); + } + + if ($foundSomething) { + return true; + } + } + + $this->seek($s); + + // closing a block + if ($this->matchChar('}', false)) { + $block = $this->popBlock(); + + if (! isset($block->type) || $block->type !== Type::T_IF) { + assert($this->env !== null); + + if ($this->env->parent) { + $this->append(null); // collect comments before next statement if needed + } + } + + if ($block instanceof ContentBlock) { + $include = $block->child; + assert(\is_array($include)); + unset($block->child); + $include[3] = $block; + $this->append($include, $s); + } elseif (!$block instanceof ElseBlock && !$block instanceof ElseifBlock) { + $type = isset($block->type) ? $block->type : Type::T_BLOCK; + $this->append([$type, $block], $s); + } + + // collect comments just after the block closing if needed + if ($this->eatWhiteDefault) { + $this->whitespace(); + assert($this->env !== null); + + if ($this->env->comments) { + $this->append(null); + } + } + + return true; + } + + // extra stuff + if ($this->matchChar(';')) { + return true; + } + + return false; + } + + /** + * Push block onto parse tree + * + * @param array|null $selectors + * @param int $pos + * + * @return Block + */ + protected function pushBlock($selectors, $pos = 0) + { + $b = new Block(); + $b->selectors = $selectors; + + $this->registerPushedBlock($b, $pos); + + return $b; + } + + /** + * @param Block $b + * @param int $pos + * + * @return void + */ + private function registerPushedBlock(Block $b, $pos) + { + list($line, $column) = $this->getSourcePosition($pos); + + $b->sourceName = $this->sourceName; + $b->sourceLine = $line; + $b->sourceColumn = $column; + $b->sourceIndex = $this->sourceIndex; + $b->comments = []; + $b->parent = $this->env; + + if (! $this->env) { + $b->children = []; + } elseif (empty($this->env->children)) { + $this->env->children = $this->env->comments; + $b->children = []; + $this->env->comments = []; + } else { + $b->children = $this->env->comments; + $this->env->comments = []; + } + + $this->env = $b; + + // collect comments at the beginning of a block if needed + if ($this->eatWhiteDefault) { + $this->whitespace(); + assert($this->env !== null); + + if ($this->env->comments) { + $this->append(null); + } + } + } + + /** + * Push special (named) block onto parse tree + * + * @deprecated + * + * @param string $type + * @param int $pos + * + * @return Block + */ + protected function pushSpecialBlock($type, $pos) + { + $block = $this->pushBlock(null, $pos); + $block->type = $type; + + return $block; + } + + /** + * Pop scope and return last block + * + * @return Block + * + * @throws \Exception + */ + protected function popBlock() + { + assert($this->env !== null); + + // collect comments ending just before of a block closing + if ($this->env->comments) { + $this->append(null); + } + + // pop the block + $block = $this->env; + + if (empty($block->parent)) { + throw $this->parseError('unexpected }'); + } + + if ($block->type == Type::T_AT_ROOT) { + // keeps the parent in case of self selector & + $block->selfParent = $block->parent; + } + + $this->env = $block->parent; + + unset($block->parent); + + return $block; + } + + /** + * Peek input stream + * + * @param string $regex + * @param array $out + * @param int $from + * + * @return int + */ + protected function peek($regex, &$out, $from = null) + { + if (! isset($from)) { + $from = $this->count; + } + + $r = '/' . $regex . '/' . $this->patternModifiers; + $result = preg_match($r, $this->buffer, $out, 0, $from); + + return $result; + } + + /** + * Seek to position in input stream (or return current position in input stream) + * + * @param int $where + * + * @return void + */ + protected function seek($where) + { + $this->count = $where; + } + + /** + * Assert a parsed part is plain CSS Valid + * + * @param array|false $parsed + * @param int $startPos + * + * @return array + * + * @throws ParserException + */ + protected function assertPlainCssValid($parsed, $startPos = null) + { + $type = ''; + if ($parsed) { + $type = $parsed[0]; + $parsed = $this->isPlainCssValidElement($parsed); + } + if (! $parsed) { + if (! \is_null($startPos)) { + $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos)); + $message = "Error : `{$plain}` isn't allowed in plain CSS"; + } else { + $message = 'Error: SCSS syntax not allowed in CSS file'; + } + if ($type) { + $message .= " ($type)"; + } + throw $this->parseError($message); + } + + return $parsed; + } + + /** + * Check a parsed element is plain CSS Valid + * + * @param array $parsed + * @param bool $allowExpression + * + * @return array|false + */ + protected function isPlainCssValidElement($parsed, $allowExpression = false) + { + // keep string as is + if (is_string($parsed)) { + return $parsed; + } + + if ( + \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) && + !\in_array($parsed[1], [ + 'alpha', + 'attr', + 'calc', + 'cubic-bezier', + 'env', + 'grayscale', + 'hsl', + 'hsla', + 'hwb', + 'invert', + 'linear-gradient', + 'min', + 'max', + 'radial-gradient', + 'repeating-linear-gradient', + 'repeating-radial-gradient', + 'rgb', + 'rgba', + 'rotate', + 'saturate', + 'var', + ]) && + Compiler::isNativeFunction($parsed[1]) + ) { + return false; + } + + switch ($parsed[0]) { + case Type::T_BLOCK: + case Type::T_KEYWORD: + case Type::T_NULL: + case Type::T_NUMBER: + case Type::T_MEDIA: + return $parsed; + + case Type::T_COMMENT: + if (isset($parsed[2])) { + return false; + } + return $parsed; + + case Type::T_DIRECTIVE: + if (\is_array($parsed[1])) { + $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]); + if (! $parsed[1][1]) { + return false; + } + } + + return $parsed; + + case Type::T_IMPORT: + if ($parsed[1][0] === Type::T_LIST) { + return false; + } + $parsed[1] = $this->isPlainCssValidElement($parsed[1]); + if ($parsed[1] === false) { + return false; + } + return $parsed; + + case Type::T_STRING: + foreach ($parsed[2] as $k => $substr) { + if (\is_array($substr)) { + $parsed[2][$k] = $this->isPlainCssValidElement($substr); + if (! $parsed[2][$k]) { + return false; + } + } + } + return $parsed; + + case Type::T_LIST: + if (!empty($parsed['enclosing'])) { + return false; + } + foreach ($parsed[2] as $k => $listElement) { + $parsed[2][$k] = $this->isPlainCssValidElement($listElement); + if (! $parsed[2][$k]) { + return false; + } + } + return $parsed; + + case Type::T_ASSIGN: + foreach ([1, 2, 3] as $k) { + if (! empty($parsed[$k])) { + $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]); + if (! $parsed[$k]) { + return false; + } + } + } + return $parsed; + + case Type::T_EXPRESSION: + list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed; + if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) { + return false; + } + $lhs = $this->isPlainCssValidElement($lhs, true); + if (! $lhs) { + return false; + } + $rhs = $this->isPlainCssValidElement($rhs, true); + if (! $rhs) { + return false; + } + + return [ + Type::T_STRING, + '', [ + $this->inParens ? '(' : '', + $lhs, + ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''), + $rhs, + $this->inParens ? ')' : '' + ] + ]; + + case Type::T_CUSTOM_PROPERTY: + case Type::T_UNARY: + $parsed[2] = $this->isPlainCssValidElement($parsed[2]); + if (! $parsed[2]) { + return false; + } + return $parsed; + + case Type::T_FUNCTION: + $argsList = $parsed[2]; + foreach ($argsList[2] as $argElement) { + if (! $this->isPlainCssValidElement($argElement)) { + return false; + } + } + return $parsed; + + case Type::T_FUNCTION_CALL: + $parsed[0] = Type::T_FUNCTION; + $argsList = [Type::T_LIST, ',', []]; + foreach ($parsed[2] as $arg) { + if ($arg[0] || ! empty($arg[2])) { + // no named arguments possible in a css function call + // nor ... argument + return false; + } + $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc'); + if (! $arg) { + return false; + } + $argsList[2][] = $arg; + } + $parsed[2] = $argsList; + return $parsed; + } + + return false; + } + + /** + * Match string looking for either ending delim, escape, or string interpolation + * + * {@internal This is a workaround for preg_match's 250K string match limit. }} + * + * @param array $m Matches (passed by reference) + * @param string $delim Delimiter + * + * @return bool True if match; false otherwise + * + * @phpstan-impure + */ + protected function matchString(&$m, $delim) + { + $token = null; + + $end = \strlen($this->buffer); + + // look for either ending delim, escape, or string interpolation + foreach (['#{', '\\', "\r", $delim] as $lookahead) { + $pos = strpos($this->buffer, $lookahead, $this->count); + + if ($pos !== false && $pos < $end) { + $end = $pos; + $token = $lookahead; + } + } + + if (! isset($token)) { + return false; + } + + $match = substr($this->buffer, $this->count, $end - $this->count); + $m = [ + $match . $token, + $match, + $token + ]; + $this->count = $end + \strlen($token); + + return true; + } + + /** + * Try to match something on head of buffer + * + * @param string $regex + * @param array $out + * @param bool $eatWhitespace + * + * @return bool + * + * @phpstan-impure + */ + protected function match($regex, &$out, $eatWhitespace = null) + { + $r = '/' . $regex . '/' . $this->patternModifiers; + + if (! preg_match($r, $this->buffer, $out, 0, $this->count)) { + return false; + } + + $this->count += \strlen($out[0]); + + if (! isset($eatWhitespace)) { + $eatWhitespace = $this->eatWhiteDefault; + } + + if ($eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + /** + * Match a single string + * + * @param string $char + * @param bool $eatWhitespace + * + * @return bool + * + * @phpstan-impure + */ + protected function matchChar($char, $eatWhitespace = null) + { + if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) { + return false; + } + + $this->count++; + + if (! isset($eatWhitespace)) { + $eatWhitespace = $this->eatWhiteDefault; + } + + if ($eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + /** + * Match literal string + * + * @param string $what + * @param int $len + * @param bool $eatWhitespace + * + * @return bool + * + * @phpstan-impure + */ + protected function literal($what, $len, $eatWhitespace = null) + { + if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { + return false; + } + + $this->count += $len; + + if (! isset($eatWhitespace)) { + $eatWhitespace = $this->eatWhiteDefault; + } + + if ($eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + /** + * Match some whitespace + * + * @return bool + * + * @phpstan-impure + */ + protected function whitespace() + { + $gotWhite = false; + + while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) { + if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { + // comment that are kept in the output CSS + $comment = []; + $startCommentCount = $this->count; + $endCommentCount = $this->count + \strlen($m[1]); + + // find interpolations in comment + $p = strpos($this->buffer, '#{', $this->count); + + while ($p !== false && $p < $endCommentCount) { + $c = substr($this->buffer, $this->count, $p - $this->count); + $comment[] = $c; + $this->count = $p; + $out = null; + + if ($this->interpolation($out)) { + // keep right spaces in the following string part + if ($out[3]) { + while ($this->buffer[$this->count - 1] !== '}') { + $this->count--; + } + + $out[3] = ''; + } + + $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out]; + } else { + list($line, $column) = $this->getSourcePosition($this->count); + $file = $this->sourceName; + if (!$this->discardComments) { + $this->logger->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); + } + $comment[] = substr($this->buffer, $this->count, 2); + + $this->count += 2; + } + + $p = strpos($this->buffer, '#{', $this->count); + } + + // remaining part + $c = substr($this->buffer, $this->count, $endCommentCount - $this->count); + + if (! $comment) { + // single part static comment + $commentStatement = [Type::T_COMMENT, $c]; + } else { + $comment[] = $c; + $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount); + $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]; + } + + list($line, $column) = $this->getSourcePosition($startCommentCount); + $commentStatement[self::SOURCE_LINE] = $line; + $commentStatement[self::SOURCE_COLUMN] = $column; + $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex; + + $this->appendComment($commentStatement); + + $this->commentsSeen[$startCommentCount] = true; + $this->count = $endCommentCount; + } else { + // comment that are ignored and not kept in the output css + $this->count += \strlen($m[0]); + // silent comments are not allowed in plain CSS files + ! $this->cssOnly + || ! \strlen(trim($m[0])) + || $this->assertPlainCssValid(false, $this->count - \strlen($m[0])); + } + + $gotWhite = true; + } + + return $gotWhite; + } + + /** + * Append comment to current block + * + * @param array $comment + * + * @return void + */ + protected function appendComment($comment) + { + assert($this->env !== null); + + if (! $this->discardComments) { + $this->env->comments[] = $comment; + } + } + + /** + * Append statement to current block + * + * @param array|null $statement + * @param int $pos + * + * @return void + */ + protected function append($statement, $pos = null) + { + assert($this->env !== null); + + if (! \is_null($statement)) { + ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); + + if (! \is_null($pos)) { + list($line, $column) = $this->getSourcePosition($pos); + + $statement[static::SOURCE_LINE] = $line; + $statement[static::SOURCE_COLUMN] = $column; + $statement[static::SOURCE_INDEX] = $this->sourceIndex; + } + + $this->env->children[] = $statement; + } + + $comments = $this->env->comments; + + if ($comments) { + $this->env->children = array_merge($this->env->children, $comments); + $this->env->comments = []; + } + } + + /** + * Returns last child was appended + * + * @return array|null + */ + protected function last() + { + assert($this->env !== null); + + $i = \count($this->env->children) - 1; + + if (isset($this->env->children[$i])) { + return $this->env->children[$i]; + } + + return null; + } + + /** + * Parse media query list + * + * @param array $out + * + * @return bool + */ + protected function mediaQueryList(&$out) + { + return $this->genericList($out, 'mediaQuery', ',', false); + } + + /** + * Parse media query + * + * @param array $out + * + * @return bool + */ + protected function mediaQuery(&$out) + { + $expressions = null; + $parts = []; + + if ( + ($this->literal('only', 4) && ($only = true) || + $this->literal('not', 3) && ($not = true) || true) && + $this->mixedKeyword($mediaType) + ) { + $prop = [Type::T_MEDIA_TYPE]; + + if (isset($only)) { + $prop[] = [Type::T_KEYWORD, 'only']; + } + + if (isset($not)) { + $prop[] = [Type::T_KEYWORD, 'not']; + } + + $media = [Type::T_LIST, '', []]; + + foreach ((array) $mediaType as $type) { + if (\is_array($type)) { + $media[2][] = $type; + } else { + $media[2][] = [Type::T_KEYWORD, $type]; + } + } + + $prop[] = $media; + $parts[] = $prop; + } + + if (empty($parts) || $this->literal('and', 3)) { + $this->genericList($expressions, 'mediaExpression', 'and', false); + + if (\is_array($expressions)) { + $parts = array_merge($parts, $expressions[2]); + } + } + + $out = $parts; + + return true; + } + + /** + * Parse supports query + * + * @param array $out + * + * @return bool + */ + protected function supportsQuery(&$out) + { + $expressions = null; + $parts = []; + + $s = $this->count; + + $not = false; + + if ( + ($this->literal('not', 3) && ($not = true) || true) && + $this->matchChar('(') && + ($this->expression($property)) && + $this->literal(': ', 2) && + $this->valueList($value) && + $this->matchChar(')') + ) { + $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]]; + $support[2][] = $property; + $support[2][] = [Type::T_KEYWORD, ': ']; + $support[2][] = $value; + $support[2][] = [Type::T_KEYWORD, ')']; + + $parts[] = $support; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->matchChar('(') && + $this->supportsQuery($subQuery) && + $this->matchChar(')') + ) { + $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]]; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('not', 3) && + $this->supportsQuery($subQuery) + ) { + $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]]; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('selector(', 9) && + $this->selector($selector) && + $this->matchChar(')') + ) { + $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]]; + + $selectorList = [Type::T_LIST, '', []]; + + foreach ($selector as $sc) { + $compound = [Type::T_STRING, '', []]; + + foreach ($sc as $scp) { + if (\is_array($scp)) { + $compound[2][] = $scp; + } else { + $compound[2][] = [Type::T_KEYWORD, $scp]; + } + } + + $selectorList[2][] = $compound; + } + + $support[2][] = $selectorList; + $support[2][] = [Type::T_KEYWORD, ')']; + $parts[] = $support; + $s = $this->count; + } else { + $this->seek($s); + } + + if ($this->variable($var) or $this->interpolation($var)) { + $parts[] = $var; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('and', 3) && + $this->genericList($expressions, 'supportsQuery', ' and', false) + ) { + array_unshift($expressions[2], [Type::T_STRING, '', $parts]); + + $parts = [$expressions]; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('or', 2) && + $this->genericList($expressions, 'supportsQuery', ' or', false) + ) { + array_unshift($expressions[2], [Type::T_STRING, '', $parts]); + + $parts = [$expressions]; + $s = $this->count; + } else { + $this->seek($s); + } + + if (\count($parts)) { + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + $out = [Type::T_STRING, '', $parts]; + + return true; + } + + return false; + } + + + /** + * Parse media expression + * + * @param array $out + * + * @return bool + */ + protected function mediaExpression(&$out) + { + $s = $this->count; + $value = null; + + if ( + $this->matchChar('(') && + $this->expression($feature) && + ($this->matchChar(':') && + $this->expression($value) || true) && + $this->matchChar(')') + ) { + $out = [Type::T_MEDIA_EXPRESSION, $feature]; + + if ($value) { + $out[] = $value; + } + + return true; + } + + $this->seek($s); + + return false; + } + + /** + * Parse argument values + * + * @param array $out + * + * @return bool + */ + protected function argValues(&$out) + { + $discardComments = $this->discardComments; + $this->discardComments = true; + + if ($this->genericList($list, 'argValue', ',', false)) { + $out = $list[2]; + + $this->discardComments = $discardComments; + + return true; + } + + $this->discardComments = $discardComments; + + return false; + } + + /** + * Parse argument value + * + * @param array $out + * + * @return bool + */ + protected function argValue(&$out) + { + $s = $this->count; + + $keyword = null; + + if (! $this->variable($keyword) || ! $this->matchChar(':')) { + $this->seek($s); + + $keyword = null; + } + + if ($this->genericList($value, 'expression', '', true)) { + $out = [$keyword, $value, false]; + $s = $this->count; + + if ($this->literal('...', 3)) { + $out[2] = true; + } else { + $this->seek($s); + } + + return true; + } + + return false; + } + + /** + * Check if a generic directive is known to be able to allow almost any syntax or not + * @param mixed $directiveName + * @return bool + */ + protected function isKnownGenericDirective($directiveName) + { + if (\is_array($directiveName) && \is_string(reset($directiveName))) { + $directiveName = reset($directiveName); + } + if (! \is_string($directiveName)) { + return false; + } + if ( + \in_array($directiveName, [ + 'at-root', + 'media', + 'mixin', + 'include', + 'scssphp-import-once', + 'import', + 'extend', + 'function', + 'break', + 'continue', + 'return', + 'each', + 'while', + 'for', + 'if', + 'debug', + 'warn', + 'error', + 'content', + 'else', + 'charset', + 'supports', + // Todo + 'use', + 'forward', + ]) + ) { + return true; + } + return false; + } + + /** + * Parse directive value list that considers $vars as keyword + * + * @param array $out + * @param string|false $endChar + * + * @return bool + * + * @phpstan-impure + */ + protected function directiveValue(&$out, $endChar = false) + { + $s = $this->count; + + if ($this->variable($out)) { + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + + if (! $endChar && $this->end()) { + return true; + } + } + + $this->seek($s); + + if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) { + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + $ss = $this->count; + if (!$endChar && $this->end()) { + $this->seek($ss); + return true; + } + } + + $this->seek($s); + + $allowVars = $this->allowVars; + $this->allowVars = false; + + $res = $this->genericList($out, 'spaceList', ','); + $this->allowVars = $allowVars; + + if ($res) { + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + + if (! $endChar && $this->end()) { + return true; + } + } + + $this->seek($s); + + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + + return false; + } + + /** + * Parse comma separated value list + * + * @param array $out + * + * @return bool + */ + protected function valueList(&$out) + { + $discardComments = $this->discardComments; + $this->discardComments = true; + $res = $this->genericList($out, 'spaceList', ','); + $this->discardComments = $discardComments; + + return $res; + } + + /** + * Parse a function call, where externals () are part of the call + * and not of the value list + * + * @param array $out + * @param bool $mandatoryEnclos + * @param null|string $charAfter + * @param null|bool $eatWhiteSp + * + * @return bool + */ + protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null) + { + $s = $this->count; + + if ( + $this->matchChar('(') && + $this->valueList($out) && + $this->matchChar(')') && + ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) + ) { + return true; + } + + if (! $mandatoryEnclos) { + $this->seek($s); + + if ( + $this->valueList($out) && + ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) + ) { + return true; + } + } + + $this->seek($s); + + return false; + } + + /** + * Parse space separated value list + * + * @param array $out + * + * @return bool + */ + protected function spaceList(&$out) + { + return $this->genericList($out, 'expression'); + } + + /** + * Parse generic list + * + * @param array $out + * @param string $parseItem The name of the method used to parse items + * @param string $delim + * @param bool $flatten + * + * @return bool + */ + protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) + { + $s = $this->count; + $items = []; + /** @var array|Number|null $value */ + $value = null; + + while ($this->$parseItem($value)) { + $trailing_delim = false; + $items[] = $value; + + if ($delim) { + if (! $this->literal($delim, \strlen($delim))) { + break; + } + + $trailing_delim = true; + } else { + assert(\is_array($value) || $value instanceof Number); + // if no delim watch that a keyword didn't eat the single/double quote + // from the following starting string + if ($value[0] === Type::T_KEYWORD) { + assert(\is_array($value)); + /** @var string $word */ + $word = $value[1]; + + $last_char = substr($word, -1); + + if ( + strlen($word) > 1 && + in_array($last_char, [ "'", '"']) && + substr($word, -2, 1) !== '\\' + ) { + // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake + $word = str_replace('\\' . $last_char, '\\\\', $word); + if (strpos($word, $last_char) < strlen($word) - 1) { + continue; + } + + $currentCount = $this->count; + + // let's try to rewind to previous char and try a parse + $this->count--; + // in case the keyword also eat spaces + while (substr($this->buffer, $this->count, 1) !== $last_char) { + $this->count--; + } + + /** @var array|Number|null $nextValue */ + $nextValue = null; + if ($this->$parseItem($nextValue)) { + assert(\is_array($nextValue) || $nextValue instanceof Number); + if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) { + // bad try, forget it + $this->seek($currentCount); + continue; + } + if ($nextValue[0] !== Type::T_STRING) { + // bad try, forget it + $this->seek($currentCount); + continue; + } + + // OK it was a good idea + $value[1] = substr($value[1], 0, -1); + array_pop($items); + $items[] = $value; + $items[] = $nextValue; + } else { + // bad try, forget it + $this->seek($currentCount); + continue; + } + } + } + } + } + + if (! $items) { + $this->seek($s); + + return false; + } + + if ($trailing_delim) { + $items[] = [Type::T_NULL]; + } + + if ($flatten && \count($items) === 1) { + $out = $items[0]; + } else { + $out = [Type::T_LIST, $delim, $items]; + } + + return true; + } + + /** + * Parse expression + * + * @param array $out + * @param bool $listOnly + * @param bool $lookForExp + * + * @return bool + * + * @phpstan-impure + */ + protected function expression(&$out, $listOnly = false, $lookForExp = true) + { + $s = $this->count; + $discard = $this->discardComments; + $this->discardComments = true; + $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]); + + if ($this->matchChar('(')) { + if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) { + if ($lookForExp) { + $out = $this->expHelper($lhs, 0); + } else { + $out = $lhs; + } + + $this->discardComments = $discard; + + return true; + } + + $this->seek($s); + } + + if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { + if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) { + if ($lookForExp) { + $out = $this->expHelper($lhs, 0); + } else { + $out = $lhs; + } + + $this->discardComments = $discard; + + return true; + } + + $this->seek($s); + } + + if (! $listOnly && $this->value($lhs)) { + if ($lookForExp) { + $out = $this->expHelper($lhs, 0); + } else { + $out = $lhs; + } + + $this->discardComments = $discard; + + return true; + } + + $this->discardComments = $discard; + + return false; + } + + /** + * Parse expression specifically checking for lists in parenthesis or brackets + * + * @param array $out + * @param int $s + * @param string $closingParen + * @param string[] $allowedTypes + * + * @return bool + * + * @phpstan-param array $allowedTypes + */ + protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]) + { + if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) { + $out = [Type::T_LIST, '', []]; + + switch ($closingParen) { + case ')': + $out['enclosing'] = 'parent'; // parenthesis list + break; + + case ']': + $out['enclosing'] = 'bracket'; // bracketed list + break; + } + + return true; + } + + if ( + $this->valueList($out) && + $this->matchChar($closingParen) && ! ($closingParen === ')' && + \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) && + \in_array(Type::T_LIST, $allowedTypes) + ) { + if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) { + $out = [Type::T_LIST, '', [$out]]; + } + + switch ($closingParen) { + case ')': + $out['enclosing'] = 'parent'; // parenthesis list + break; + + case ']': + $out['enclosing'] = 'bracket'; // bracketed list + break; + } + + return true; + } + + $this->seek($s); + + if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) { + return true; + } + + return false; + } + + /** + * Parse left-hand side of subexpression + * + * @param array $lhs + * @param int $minP + * + * @return array + */ + protected function expHelper($lhs, $minP) + { + $operators = static::$operatorPattern; + + $ss = $this->count; + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + while ($this->match($operators, $m, false) && static::$precedence[strtolower($m[1])] >= $minP) { + $whiteAfter = isset($this->buffer[$this->count]) && + ctype_space($this->buffer[$this->count]); + $varAfter = isset($this->buffer[$this->count]) && + $this->buffer[$this->count] === '$'; + + $this->whitespace(); + + $op = $m[1]; + + // don't turn negative numbers into expressions + if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) { + break; + } + + if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) { + break; + } + + if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) { + break; + } + + // consume higher-precedence operators on the right-hand side + $rhs = $this->expHelper($rhs, static::$precedence[strtolower($op)] + 1); + + $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; + + $ss = $this->count; + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + } + + $this->seek($ss); + + return $lhs; + } + + /** + * Parse value + * + * @param array $out + * + * @return bool + */ + protected function value(&$out) + { + if (! isset($this->buffer[$this->count])) { + return false; + } + + $s = $this->count; + $char = $this->buffer[$this->count]; + + if ( + $this->literal('url(', 4) && + $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false) + ) { + $len = strspn( + $this->buffer, + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', + $this->count + ); + + $this->count += $len; + + if ($this->matchChar(')')) { + $content = substr($this->buffer, $s, $this->count - $s); + $out = [Type::T_KEYWORD, $content]; + + return true; + } + } + + $this->seek($s); + + if ( + $this->literal('url(', 4, false) && + $this->match('\s*(\/\/[^\s\)]+)\s*', $m) + ) { + $content = 'url(' . $m[1]; + + if ($this->matchChar(')')) { + $content .= ')'; + $out = [Type::T_KEYWORD, $content]; + + return true; + } + } + + $this->seek($s); + + // not + if ($char === 'n' && $this->literal('not', 3, false)) { + if ( + $this->whitespace() && + $this->value($inner) + ) { + $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; + + return true; + } + + $this->seek($s); + + if ($this->parenValue($inner)) { + $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; + + return true; + } + + $this->seek($s); + } + + // addition + if ($char === '+') { + $this->count++; + + $follow_white = $this->whitespace(); + + if ($this->value($inner)) { + $out = [Type::T_UNARY, '+', $inner, $this->inParens]; + + return true; + } + + if ($follow_white) { + $out = [Type::T_KEYWORD, $char]; + return true; + } + + $this->seek($s); + + return false; + } + + // negation + if ($char === '-') { + if ($this->customProperty($out)) { + return true; + } + + $this->count++; + + $follow_white = $this->whitespace(); + + if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) { + $out = [Type::T_UNARY, '-', $inner, $this->inParens]; + + return true; + } + + if ( + $this->keyword($inner) && + ! $this->func($inner, $out) + ) { + $out = [Type::T_UNARY, '-', $inner, $this->inParens]; + + return true; + } + + if ($follow_white) { + $out = [Type::T_KEYWORD, $char]; + + return true; + } + + $this->seek($s); + } + + // paren + if ($char === '(' && $this->parenValue($out)) { + return true; + } + + if ($char === '#') { + if ($this->interpolation($out) || $this->color($out)) { + return true; + } + + $this->count++; + + if ($this->keyword($keyword)) { + $out = [Type::T_KEYWORD, '#' . $keyword]; + + return true; + } + + $this->count--; + } + + if ($this->matchChar('&', true)) { + $out = [Type::T_SELF]; + + return true; + } + + if ($char === '$' && $this->variable($out)) { + return true; + } + + if ($char === 'p' && $this->progid($out)) { + return true; + } + + if (($char === '"' || $char === "'") && $this->string($out)) { + return true; + } + + if ($this->unit($out)) { + return true; + } + + // unicode range with wildcards + if ( + $this->literal('U+', 2) && + $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false) + ) { + $unicode = explode('-', $m[0]); + if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) { + $out = [Type::T_KEYWORD, 'U+' . $m[0]]; + + return true; + } + $this->count -= strlen($m[0]) + 2; + } + + if ($this->keyword($keyword, false)) { + if ($this->func($keyword, $out)) { + return true; + } + + $this->whitespace(); + + if ($keyword === 'null') { + $out = [Type::T_NULL]; + } else { + $out = [Type::T_KEYWORD, $keyword]; + } + + return true; + } + + return false; + } + + /** + * Parse parenthesized value + * + * @param array $out + * + * @return bool + */ + protected function parenValue(&$out) + { + $s = $this->count; + + $inParens = $this->inParens; + + if ($this->matchChar('(')) { + if ($this->matchChar(')')) { + $out = [Type::T_LIST, '', []]; + + return true; + } + + $this->inParens = true; + + if ( + $this->expression($exp) && + $this->matchChar(')') + ) { + $out = $exp; + $this->inParens = $inParens; + + return true; + } + } + + $this->inParens = $inParens; + $this->seek($s); + + return false; + } + + /** + * Parse "progid:" + * + * @param array $out + * + * @return bool + */ + protected function progid(&$out) + { + $s = $this->count; + + if ( + $this->literal('progid:', 7, false) && + $this->openString('(', $fn) && + $this->matchChar('(') + ) { + $this->openString(')', $args, '('); + + if ($this->matchChar(')')) { + $out = [Type::T_STRING, '', [ + 'progid:', $fn, '(', $args, ')' + ]]; + + return true; + } + } + + $this->seek($s); + + return false; + } + + /** + * Parse function call + * + * @param string $name + * @param array $func + * + * @return bool + */ + protected function func($name, &$func) + { + $s = $this->count; + + if ($this->matchChar('(')) { + if ($name === 'alpha' && $this->argumentList($args)) { + $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]]; + + return true; + } + + if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) { + $ss = $this->count; + + if ( + $this->argValues($args) && + $this->matchChar(')') + ) { + if (strtolower($name) === 'var' && \count($args) === 2 && $args[1][0] === Type::T_NULL) { + $args[1] = [null, [Type::T_STRING, '', [' ']], false]; + } + + $func = [Type::T_FUNCTION_CALL, $name, $args]; + + return true; + } + + $this->seek($ss); + } + + if ( + ($this->openString(')', $str, '(') || true) && + $this->matchChar(')') + ) { + $args = []; + + if (! empty($str)) { + $args[] = [null, [Type::T_STRING, '', [$str]]]; + } + + $func = [Type::T_FUNCTION_CALL, $name, $args]; + + return true; + } + } + + $this->seek($s); + + return false; + } + + /** + * Parse function call argument list + * + * @param array $out + * + * @return bool + */ + protected function argumentList(&$out) + { + $s = $this->count; + $this->matchChar('('); + + $args = []; + + while ($this->keyword($var)) { + if ( + $this->matchChar('=') && + $this->expression($exp) + ) { + $args[] = [Type::T_STRING, '', [$var . '=']]; + $arg = $exp; + } else { + break; + } + + $args[] = $arg; + + if (! $this->matchChar(',')) { + break; + } + + $args[] = [Type::T_STRING, '', [', ']]; + } + + if (! $this->matchChar(')') || ! $args) { + $this->seek($s); + + return false; + } + + $out = $args; + + return true; + } + + /** + * Parse mixin/function definition argument list + * + * @param array $out + * + * @return bool + */ + protected function argumentDef(&$out) + { + $s = $this->count; + $this->matchChar('('); + + $args = []; + + while ($this->variable($var)) { + $arg = [$var[1], null, false]; + + $ss = $this->count; + + if ( + $this->matchChar(':') && + $this->genericList($defaultVal, 'expression', '', true) + ) { + $arg[1] = $defaultVal; + } else { + $this->seek($ss); + } + + $ss = $this->count; + + if ($this->literal('...', 3)) { + $sss = $this->count; + + if (! $this->matchChar(')')) { + throw $this->parseError('... has to be after the final argument'); + } + + $arg[2] = true; + + $this->seek($sss); + } else { + $this->seek($ss); + } + + $args[] = $arg; + + if (! $this->matchChar(',')) { + break; + } + } + + if (! $this->matchChar(')')) { + $this->seek($s); + + return false; + } + + $out = $args; + + return true; + } + + /** + * Parse map + * + * @param array $out + * + * @return bool + */ + protected function map(&$out) + { + $s = $this->count; + + if (! $this->matchChar('(')) { + return false; + } + + $keys = []; + $values = []; + + while ( + $this->genericList($key, 'expression', '', true) && + $this->matchChar(':') && + $this->genericList($value, 'expression', '', true) + ) { + $keys[] = $key; + $values[] = $value; + + if (! $this->matchChar(',')) { + break; + } + } + + if (! $keys || ! $this->matchChar(')')) { + $this->seek($s); + + return false; + } + + $out = [Type::T_MAP, $keys, $values]; + + return true; + } + + /** + * Parse color + * + * @param array $out + * + * @return bool + */ + protected function color(&$out) + { + $s = $this->count; + + if ($this->match('(#([0-9a-f]+)\b)', $m)) { + if (\in_array(\strlen($m[2]), [3,4,6,8])) { + $out = [Type::T_KEYWORD, $m[0]]; + + return true; + } + + $this->seek($s); + + return false; + } + + return false; + } + + /** + * Parse number with unit + * + * @param array $unit + * + * @return bool + */ + protected function unit(&$unit) + { + $s = $this->count; + + if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) { + if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) { + $this->whitespace(); + + $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]); + + return true; + } + + $this->seek($s); + } + + return false; + } + + /** + * Parse string + * + * @param array $out + * @param bool $keepDelimWithInterpolation + * + * @return bool + */ + protected function string(&$out, $keepDelimWithInterpolation = false) + { + $s = $this->count; + + if ($this->matchChar('"', false)) { + $delim = '"'; + } elseif ($this->matchChar("'", false)) { + $delim = "'"; + } else { + return false; + } + + $content = []; + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + $hasInterpolation = false; + + while ($this->matchString($m, $delim)) { + if ($m[1] !== '') { + $content[] = $m[1]; + } + + if ($m[2] === '#{') { + $this->count -= \strlen($m[2]); + + if ($this->interpolation($inter, false)) { + $content[] = $inter; + $hasInterpolation = true; + } else { + $this->count += \strlen($m[2]); + $content[] = '#{'; // ignore it + } + } elseif ($m[2] === "\r") { + $content[] = chr(10); + // TODO : warning + # DEPRECATION WARNING on line x, column y of zzz: + # Unescaped multiline strings are deprecated and will be removed in a future version of Sass. + # To include a newline in a string, use "\a" or "\a " as in CSS. + if ($this->matchChar("\n", false)) { + $content[] = ' '; + } + } elseif ($m[2] === '\\') { + if ( + $this->literal("\r\n", 2, false) || + $this->matchChar("\r", false) || + $this->matchChar("\n", false) || + $this->matchChar("\f", false) + ) { + // this is a continuation escaping, to be ignored + } elseif ($this->matchEscapeCharacter($c)) { + $content[] = $c; + } else { + throw $this->parseError('Unterminated escape sequence'); + } + } else { + $this->count -= \strlen($delim); + break; // delim + } + } + + $this->eatWhiteDefault = $oldWhite; + + if ($this->literal($delim, \strlen($delim))) { + if ($hasInterpolation && ! $keepDelimWithInterpolation) { + $delim = '"'; + } + + $out = [Type::T_STRING, $delim, $content]; + + return true; + } + + $this->seek($s); + + return false; + } + + /** + * @param string $out + * @param bool $inKeywords + * + * @return bool + */ + protected function matchEscapeCharacter(&$out, $inKeywords = false) + { + $s = $this->count; + if ($this->match('[a-f0-9]', $m, false)) { + $hex = $m[0]; + + for ($i = 5; $i--;) { + if ($this->match('[a-f0-9]', $m, false)) { + $hex .= $m[0]; + } else { + break; + } + } + + // CSS allows Unicode escape sequences to be followed by a delimiter space + // (necessary in some cases for shorter sequences to disambiguate their end) + $this->matchChar(' ', false); + + $value = hexdec($hex); + + if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) { + $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5 + } elseif ($value < 0x20) { + $out = Util::mbChr($value); + } else { + $out = Util::mbChr($value); + } + + return true; + } + + if ($this->match('.', $m, false)) { + if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) { + $this->seek($s); + return false; + } + $out = $m[0]; + + return true; + } + + return false; + } + + /** + * Parse keyword or interpolation + * + * @param array $out + * @param bool $restricted + * + * @return bool + */ + protected function mixedKeyword(&$out, $restricted = false) + { + $parts = []; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + for (;;) { + if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) { + $parts[] = $key; + continue; + } + + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $parts) { + return false; + } + + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + $out = $parts; + + return true; + } + + /** + * Parse an unbounded string stopped by $end + * + * @param string $end + * @param array $out + * @param string $nestOpen + * @param string $nestClose + * @param bool $rtrim + * @param string $disallow + * + * @return bool + */ + protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null) + { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + if ($nestOpen && ! $nestClose) { + $nestClose = $end; + } + + $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.'); + $patt = '(' . $patt . '*?)([\'"]|#\{|' + . $this->pregQuote($end) . '|' + . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '') + . static::$commentPattern . ')'; + + $nestingLevel = 0; + + $content = []; + + while ($this->match($patt, $m, false)) { + if (isset($m[1]) && $m[1] !== '') { + $content[] = $m[1]; + + if ($nestOpen) { + $nestingLevel += substr_count($m[1], $nestOpen); + } + } + + $tok = $m[2]; + + $this->count -= \strlen($tok); + + if ($tok === $end && ! $nestingLevel) { + break; + } + + if ($tok === $nestClose) { + $nestingLevel--; + } + + if (($tok === "'" || $tok === '"') && $this->string($str, true)) { + $content[] = $str; + continue; + } + + if ($tok === '#{' && $this->interpolation($inter)) { + $content[] = $inter; + continue; + } + + $content[] = $tok; + $this->count += \strlen($tok); + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $content || $tok !== $end) { + return false; + } + + // trim the end + if ($rtrim && \is_string(end($content))) { + $content[\count($content) - 1] = rtrim(end($content)); + } + + $out = [Type::T_STRING, '', $content]; + + return true; + } + + /** + * Parser interpolation + * + * @param string|array $out + * @param bool $lookWhite save information about whitespace before and after + * + * @return bool + */ + protected function interpolation(&$out, $lookWhite = true) + { + $oldWhite = $this->eatWhiteDefault; + $allowVars = $this->allowVars; + $this->allowVars = true; + $this->eatWhiteDefault = true; + + $s = $this->count; + + if ( + $this->literal('#{', 2) && + $this->valueList($value) && + $this->matchChar('}', false) + ) { + if ($value === [Type::T_SELF]) { + $out = $value; + } else { + if ($lookWhite) { + $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : ''; + $right = ( + ! empty($this->buffer[$this->count]) && + preg_match('/\s/', $this->buffer[$this->count]) + ) ? ' ' : ''; + } else { + $left = $right = false; + } + + $out = [Type::T_INTERPOLATE, $value, $left, $right]; + } + + $this->eatWhiteDefault = $oldWhite; + $this->allowVars = $allowVars; + + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + return true; + } + + $this->seek($s); + + $this->eatWhiteDefault = $oldWhite; + $this->allowVars = $allowVars; + + return false; + } + + /** + * Parse property name (as an array of parts or a string) + * + * @param array $out + * + * @return bool + */ + protected function propertyName(&$out) + { + $parts = []; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + for (;;) { + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + if ($this->keyword($text)) { + $parts[] = $text; + continue; + } + + if (! $parts && $this->match('[:.#]', $m, false)) { + // css hacks + $parts[] = $m[0]; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $parts) { + return false; + } + + // match comment hack + if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) { + if (! empty($m[0])) { + $parts[] = $m[0]; + $this->count += \strlen($m[0]); + } + } + + $this->whitespace(); // get any extra whitespace + + $out = [Type::T_STRING, '', $parts]; + + return true; + } + + /** + * Parse custom property name (as an array of parts or a string) + * + * @param array $out + * + * @return bool + */ + protected function customProperty(&$out) + { + $s = $this->count; + + if (! $this->literal('--', 2, false)) { + return false; + } + + $parts = ['--']; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + for (;;) { + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + if ($this->matchChar('&', false)) { + $parts[] = [Type::T_SELF]; + continue; + } + + if ($this->variable($var)) { + $parts[] = $var; + continue; + } + + if ($this->keyword($text)) { + $parts[] = $text; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (\count($parts) == 1) { + $this->seek($s); + + return false; + } + + $this->whitespace(); // get any extra whitespace + + $out = [Type::T_STRING, '', $parts]; + + return true; + } + + /** + * Parse comma separated selector list + * + * @param array $out + * @param string|bool $subSelector + * + * @return bool + */ + protected function selectors(&$out, $subSelector = false) + { + $s = $this->count; + $selectors = []; + + while ($this->selector($sel, $subSelector)) { + $selectors[] = $sel; + + if (! $this->matchChar(',', true)) { + break; + } + + while ($this->matchChar(',', true)) { + ; // ignore extra + } + } + + if (! $selectors) { + $this->seek($s); + + return false; + } + + $out = $selectors; + + return true; + } + + /** + * Parse whitespace separated selector list + * + * @param array $out + * @param string|bool $subSelector + * + * @return bool + */ + protected function selector(&$out, $subSelector = false) + { + $selector = []; + + $discardComments = $this->discardComments; + $this->discardComments = true; + + for (;;) { + $s = $this->count; + + if ($this->match('[>+~]+', $m, true)) { + if ( + $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 && + $m[0] === '+' && $this->match("(\d+|n\b)", $counter) + ) { + $this->seek($s); + } else { + $selector[] = [$m[0]]; + continue; + } + } + + if ($this->selectorSingle($part, $subSelector)) { + $selector[] = $part; + $this->whitespace(); + continue; + } + + break; + } + + $this->discardComments = $discardComments; + + if (! $selector) { + return false; + } + + $out = $selector; + + return true; + } + + /** + * parsing escaped chars in selectors: + * - escaped single chars are kept escaped in the selector but in a normalized form + * (if not in 0-9a-f range as this would be ambigous) + * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form, + * normalized to lowercase + * + * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars, + * and escaping added when printing in the Compiler, where/if it's mandatory + * - but this require a better formal selector representation instead of the array we have now + * + * @param string $out + * @param bool $keepEscapedNumber + * + * @return bool + */ + protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false) + { + $s_escape = $this->count; + if ($this->match('\\\\', $m)) { + $out = '\\' . $m[0]; + return true; + } + + if ($this->matchEscapeCharacter($escapedout, true)) { + if (strlen($escapedout) === 1) { + if (!preg_match(",\w,", $escapedout)) { + $out = '\\' . $escapedout; + return true; + } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) { + $out = $escapedout; + return true; + } + } + $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape)); + if (strlen($escape_sequence) < 6) { + $escape_sequence .= ' '; + } + $out = '\\' . strtolower($escape_sequence); + return true; + } + if ($this->match('\\S', $m)) { + $out = '\\' . $m[0]; + return true; + } + + + return false; + } + + /** + * Parse the parts that make up a selector + * + * {@internal + * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder + * }} + * + * @param array $out + * @param string|bool $subSelector + * + * @return bool + */ + protected function selectorSingle(&$out, $subSelector = false) + { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + $parts = []; + + if ($this->matchChar('*', false)) { + $parts[] = '*'; + } + + for (;;) { + if (! isset($this->buffer[$this->count])) { + break; + } + + $s = $this->count; + $char = $this->buffer[$this->count]; + + // see if we can stop early + if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') { + break; + } + + // parsing a sub selector in () stop with the closing ) + if ($subSelector && $char === ')') { + break; + } + + //self + switch ($char) { + case '&': + $parts[] = Compiler::$selfSelector; + $this->count++; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + continue 2; + + case '.': + $parts[] = '.'; + $this->count++; + continue 2; + + case '|': + $parts[] = '|'; + $this->count++; + continue 2; + } + + // handling of escaping in selectors : get the escaped char + if ($char === '\\') { + $this->count++; + if ($this->matchEscapeCharacterInSelector($escaped, true)) { + $parts[] = $escaped; + continue; + } + $this->count--; + } + + if ($char === '%') { + $this->count++; + + if ($this->placeholder($placeholder)) { + $parts[] = '%'; + $parts[] = $placeholder; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + continue; + } + + break; + } + + if ($char === '#') { + if ($this->interpolation($inter)) { + $parts[] = $inter; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + continue; + } + + $parts[] = '#'; + $this->count++; + continue; + } + + // a pseudo selector + if ($char === ':') { + if ($this->buffer[$this->count + 1] === ':') { + $this->count += 2; + $part = '::'; + } else { + $this->count++; + $part = ':'; + } + + if ($this->mixedKeyword($nameParts, true)) { + $parts[] = $part; + + foreach ($nameParts as $sub) { + $parts[] = $sub; + } + + $ss = $this->count; + + if ( + $nameParts === ['not'] || + $nameParts === ['is'] || + $nameParts === ['has'] || + $nameParts === ['where'] || + $nameParts === ['slotted'] || + $nameParts === ['nth-child'] || + $nameParts === ['nth-last-child'] || + $nameParts === ['nth-of-type'] || + $nameParts === ['nth-last-of-type'] + ) { + if ( + $this->matchChar('(', true) && + ($this->selectors($subs, reset($nameParts)) || true) && + $this->matchChar(')') + ) { + $parts[] = '('; + + while ($sub = array_shift($subs)) { + while ($ps = array_shift($sub)) { + foreach ($ps as &$p) { + $parts[] = $p; + } + + if (\count($sub) && reset($sub)) { + $parts[] = ' '; + } + } + + if (\count($subs) && reset($subs)) { + $parts[] = ', '; + } + } + + $parts[] = ')'; + } else { + $this->seek($ss); + } + } elseif ( + $this->matchChar('(', true) && + ($this->openString(')', $str, '(') || true) && + $this->matchChar(')') + ) { + $parts[] = '('; + + if (! empty($str)) { + $parts[] = $str; + } + + $parts[] = ')'; + } else { + $this->seek($ss); + } + + continue; + } + } + + $this->seek($s); + + // 2n+1 + if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) { + if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) { + $parts[] = $counter[0]; + //$parts[] = str_replace(' ', '', $counter[0]); + continue; + } + } + + $this->seek($s); + + // attribute selector + if ( + $char === '[' && + $this->matchChar('[') && + ($this->openString(']', $str, '[') || true) && + $this->matchChar(']') + ) { + $parts[] = '['; + + if (! empty($str)) { + $parts[] = $str; + } + + $parts[] = ']'; + continue; + } + + $this->seek($s); + + // for keyframes + if ($this->unit($unit)) { + $parts[] = $unit; + continue; + } + + if ($this->restrictedKeyword($name, false, true)) { + $parts[] = $name; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $parts) { + return false; + } + + $out = $parts; + + return true; + } + + /** + * Parse a variable + * + * @param array $out + * + * @return bool + */ + protected function variable(&$out) + { + $s = $this->count; + + if ( + $this->matchChar('$', false) && + $this->keyword($name) + ) { + if ($this->allowVars) { + $out = [Type::T_VARIABLE, $name]; + } else { + $out = [Type::T_KEYWORD, '$' . $name]; + } + + return true; + } + + $this->seek($s); + + return false; + } + + /** + * Parse a keyword + * + * @param string $word + * @param bool $eatWhitespace + * @param bool $inSelector + * + * @return bool + */ + protected function keyword(&$word, $eatWhitespace = null, $inSelector = false) + { + $s = $this->count; + $match = $this->match( + $this->utf8 + ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)' + : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)', + $m, + false + ); + + if ($match) { + $word = $m[1]; + + // handling of escaping in keyword : get the escaped char + if (strpos($word, '\\') !== false) { + $send = $this->count; + $escapedWord = []; + $this->seek($s); + $previousEscape = false; + while ($this->count < $send) { + $char = $this->buffer[$this->count]; + $this->count++; + if ( + $this->count < $send + && $char === '\\' + && !$previousEscape + && ( + $inSelector ? + $this->matchEscapeCharacterInSelector($out) + : + $this->matchEscapeCharacter($out, true) + ) + ) { + $escapedWord[] = $out; + } else { + if ($previousEscape) { + $previousEscape = false; + } elseif ($char === '\\') { + $previousEscape = true; + } + $escapedWord[] = $char; + } + } + + $word = implode('', $escapedWord); + } + + if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + return false; + } + + /** + * Parse a keyword that should not start with a number + * + * @param string $word + * @param bool $eatWhitespace + * @param bool $inSelector + * + * @return bool + */ + protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false) + { + $s = $this->count; + + if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) { + return true; + } + + $this->seek($s); + + return false; + } + + /** + * Parse a placeholder + * + * @param string|array $placeholder + * + * @return bool + */ + protected function placeholder(&$placeholder) + { + $match = $this->match( + $this->utf8 + ? '([\pL\w\-_]+)' + : '([\w\-_]+)', + $m + ); + + if ($match) { + $placeholder = $m[1]; + + return true; + } + + if ($this->interpolation($placeholder)) { + return true; + } + + return false; + } + + /** + * Parse a url + * + * @param array $out + * + * @return bool + */ + protected function url(&$out) + { + if ($this->literal('url(', 4)) { + $s = $this->count; + + if ( + ($this->string($out) || $this->spaceList($out)) && + $this->matchChar(')') + ) { + $out = [Type::T_STRING, '', ['url(', $out, ')']]; + + return true; + } + + $this->seek($s); + + if ( + $this->openString(')', $out) && + $this->matchChar(')') + ) { + $out = [Type::T_STRING, '', ['url(', $out, ')']]; + + return true; + } + } + + return false; + } + + /** + * Consume an end of statement delimiter + * @param bool $eatWhitespace + * + * @return bool + */ + protected function end($eatWhitespace = null) + { + if ($this->matchChar(';', $eatWhitespace)) { + return true; + } + + if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') { + // if there is end of file or a closing block next then we don't need a ; + return true; + } + + return false; + } + + /** + * Strip assignment flag from the list + * + * @param array $value + * + * @return string[] + */ + protected function stripAssignmentFlags(&$value) + { + $flags = []; + + for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) { + $lastNode = &$token[2][$s - 1]; + + while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) { + array_pop($token[2]); + + $node = end($token[2]); + $token = $this->flattenList($token); + $flags[] = $lastNode[1]; + $lastNode = $node; + } + } + + return $flags; + } + + /** + * Strip optional flag from selector list + * + * @param array $selectors + * + * @return bool + */ + protected function stripOptionalFlag(&$selectors) + { + $optional = false; + $selector = end($selectors); + $part = end($selector); + + if ($part === ['!optional']) { + array_pop($selectors[\count($selectors) - 1]); + + $optional = true; + } + + return $optional; + } + + /** + * Turn list of length 1 into value type + * + * @param array $value + * + * @return array + */ + protected function flattenList($value) + { + if ($value[0] === Type::T_LIST && \count($value[2]) === 1) { + return $this->flattenList($value[2][0]); + } + + return $value; + } + + /** + * Quote regular expression + * + * @param string $what + * + * @return string + */ + private function pregQuote($what) + { + return preg_quote($what, '/'); + } + + /** + * Extract line numbers from buffer + * + * @param string $buffer + * + * @return void + */ + private function extractLineNumbers($buffer) + { + $this->sourcePositions = [0 => 0]; + $prev = 0; + + while (($pos = strpos($buffer, "\n", $prev)) !== false) { + $this->sourcePositions[] = $pos; + $prev = $pos + 1; + } + + $this->sourcePositions[] = \strlen($buffer); + + if (substr($buffer, -1) !== "\n") { + $this->sourcePositions[] = \strlen($buffer) + 1; + } + } + + /** + * Get source line number and column (given character position in the buffer) + * + * @param int $pos + * + * @return array + * @phpstan-return array{int, int} + */ + private function getSourcePosition($pos) + { + $low = 0; + $high = \count($this->sourcePositions); + + while ($low < $high) { + $mid = (int) (($high + $low) / 2); + + if ($pos < $this->sourcePositions[$mid]) { + $high = $mid - 1; + continue; + } + + if ($pos >= $this->sourcePositions[$mid + 1]) { + $low = $mid + 1; + continue; + } + + return [$mid + 1, $pos - $this->sourcePositions[$mid]]; + } + + return [$low + 1, $pos - $this->sourcePositions[$low]]; + } + + /** + * Save internal encoding of mbstring + * + * When mbstring.func_overload is used to replace the standard PHP string functions, + * this method configures the internal encoding to a single-byte one so that the + * behavior matches the normal behavior of PHP string functions while using the parser. + * The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}. + * + * If mbstring.func_overload is not used (or does not override string functions), this method is a no-op. + * + * @return void + */ + private function saveEncoding() + { + if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) { + $this->encoding = mb_internal_encoding(); + + mb_internal_encoding('iso-8859-1'); + } + } + + /** + * Restore internal encoding + * + * @return void + */ + private function restoreEncoding() + { + if (\extension_loaded('mbstring') && $this->encoding) { + mb_internal_encoding($this->encoding); + } + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php b/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php new file mode 100644 index 0000000..00b6b45 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php @@ -0,0 +1,187 @@ + + * + * @internal + */ +class Base64 +{ + /** + * @var array + */ + private static $encodingMap = [ + 0 => 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + 4 => 'E', + 5 => 'F', + 6 => 'G', + 7 => 'H', + 8 => 'I', + 9 => 'J', + 10 => 'K', + 11 => 'L', + 12 => 'M', + 13 => 'N', + 14 => 'O', + 15 => 'P', + 16 => 'Q', + 17 => 'R', + 18 => 'S', + 19 => 'T', + 20 => 'U', + 21 => 'V', + 22 => 'W', + 23 => 'X', + 24 => 'Y', + 25 => 'Z', + 26 => 'a', + 27 => 'b', + 28 => 'c', + 29 => 'd', + 30 => 'e', + 31 => 'f', + 32 => 'g', + 33 => 'h', + 34 => 'i', + 35 => 'j', + 36 => 'k', + 37 => 'l', + 38 => 'm', + 39 => 'n', + 40 => 'o', + 41 => 'p', + 42 => 'q', + 43 => 'r', + 44 => 's', + 45 => 't', + 46 => 'u', + 47 => 'v', + 48 => 'w', + 49 => 'x', + 50 => 'y', + 51 => 'z', + 52 => '0', + 53 => '1', + 54 => '2', + 55 => '3', + 56 => '4', + 57 => '5', + 58 => '6', + 59 => '7', + 60 => '8', + 61 => '9', + 62 => '+', + 63 => '/', + ]; + + /** + * @var array + */ + private static $decodingMap = [ + 'A' => 0, + 'B' => 1, + 'C' => 2, + 'D' => 3, + 'E' => 4, + 'F' => 5, + 'G' => 6, + 'H' => 7, + 'I' => 8, + 'J' => 9, + 'K' => 10, + 'L' => 11, + 'M' => 12, + 'N' => 13, + 'O' => 14, + 'P' => 15, + 'Q' => 16, + 'R' => 17, + 'S' => 18, + 'T' => 19, + 'U' => 20, + 'V' => 21, + 'W' => 22, + 'X' => 23, + 'Y' => 24, + 'Z' => 25, + 'a' => 26, + 'b' => 27, + 'c' => 28, + 'd' => 29, + 'e' => 30, + 'f' => 31, + 'g' => 32, + 'h' => 33, + 'i' => 34, + 'j' => 35, + 'k' => 36, + 'l' => 37, + 'm' => 38, + 'n' => 39, + 'o' => 40, + 'p' => 41, + 'q' => 42, + 'r' => 43, + 's' => 44, + 't' => 45, + 'u' => 46, + 'v' => 47, + 'w' => 48, + 'x' => 49, + 'y' => 50, + 'z' => 51, + 0 => 52, + 1 => 53, + 2 => 54, + 3 => 55, + 4 => 56, + 5 => 57, + 6 => 58, + 7 => 59, + 8 => 60, + 9 => 61, + '+' => 62, + '/' => 63, + ]; + + /** + * Convert to base64 + * + * @param int $value + * + * @return string + */ + public static function encode($value) + { + return self::$encodingMap[$value]; + } + + /** + * Convert from base64 + * + * @param string $value + * + * @return int + */ + public static function decode($value) + { + return self::$decodingMap[$value]; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php b/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php new file mode 100644 index 0000000..2a5210c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php @@ -0,0 +1,151 @@ + + * @author Anthon Pang + * + * @internal + */ +class Base64VLQ +{ + // A Base64 VLQ digit can represent 5 bits, so it is base-32. + const VLQ_BASE_SHIFT = 5; + + // A mask of bits for a VLQ digit (11111), 31 decimal. + const VLQ_BASE_MASK = 31; + + // The continuation bit is the 6th bit. + const VLQ_CONTINUATION_BIT = 32; + + /** + * Returns the VLQ encoded value. + * + * @param int $value + * + * @return string + */ + public static function encode($value) + { + $encoded = ''; + $vlq = self::toVLQSigned($value); + + do { + $digit = $vlq & self::VLQ_BASE_MASK; + + //$vlq >>>= self::VLQ_BASE_SHIFT; // unsigned right shift + $vlq = (($vlq >> 1) & PHP_INT_MAX) >> (self::VLQ_BASE_SHIFT - 1); + + if ($vlq > 0) { + $digit |= self::VLQ_CONTINUATION_BIT; + } + + $encoded .= Base64::encode($digit); + } while ($vlq > 0); + + return $encoded; + } + + /** + * Decodes VLQValue. + * + * @param string $str + * @param int $index + * + * @return int + */ + public static function decode($str, &$index) + { + $result = 0; + $shift = 0; + + do { + $c = $str[$index++]; + $digit = Base64::decode($c); + $continuation = ($digit & self::VLQ_CONTINUATION_BIT) != 0; + $digit &= self::VLQ_BASE_MASK; + $result = $result + ($digit << $shift); + $shift = $shift + self::VLQ_BASE_SHIFT; + } while ($continuation); + + return self::fromVLQSigned($result); + } + + /** + * Converts from a two-complement value to a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) + * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) + * + * @param int $value + * + * @return int + */ + private static function toVLQSigned($value) + { + if ($value < 0) { + return ((-$value) << 1) + 1; + } + + return ($value << 1) + 0; + } + + /** + * Converts to a two-complement value from a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 + * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 + * + * @param int $value + * + * @return int + */ + private static function fromVLQSigned($value) + { + $negate = ($value & 1) === 1; + + //$value >>>= 1; // unsigned right shift + $value = ($value >> 1) & PHP_INT_MAX; + + if (! $negate) { + return $value; + } + + // We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit) is + // always set for negative numbers. If `value` were 1, (meaning `negate` is + // true and all other bits were zeros), `value` would now be 0. -0 is just + // 0, and doesn't flip the 32nd bit as intended. All positive numbers will + // successfully flip the 32nd bit without issue, so it's a noop for them. + return -$value | 0x80000000; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php b/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php new file mode 100644 index 0000000..ccd4f02 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php @@ -0,0 +1,390 @@ + + * @author Nicolas FRANÇOIS + * + * @internal + */ +class SourceMapGenerator +{ + /** + * What version of source map does the generator generate? + */ + const VERSION = 3; + + /** + * Array of default options + * + * @var array + * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string} + */ + protected $defaultOptions = [ + // an optional source root, useful for relocating source files + // on a server or removing repeated values in the 'sources' entry. + // This value is prepended to the individual entries in the 'source' field. + 'sourceRoot' => '', + + // an optional name of the generated code that this source map is associated with. + 'sourceMapFilename' => null, + + // url of the map + 'sourceMapURL' => null, + + // absolute path to a file to write the map to + 'sourceMapWriteTo' => null, + + // output source contents? + 'outputSourceFiles' => false, + + // base path for filename normalization + 'sourceMapRootpath' => '', + + // base path for filename normalization + 'sourceMapBasepath' => '' + ]; + + /** + * The base64 VLQ encoder + * + * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ + */ + protected $encoder; + + /** + * Array of mappings + * + * @var array + * @phpstan-var list + */ + protected $mappings = []; + + /** + * Array of contents map + * + * @var array + */ + protected $contentsMap = []; + + /** + * File to content map + * + * @var array + */ + protected $sources = []; + + /** + * @var array + */ + protected $sourceKeys = []; + + /** + * @var array + * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string} + */ + private $options; + + /** + * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $options + */ + public function __construct(array $options = []) + { + $this->options = array_replace($this->defaultOptions, $options); + $this->encoder = new Base64VLQ(); + } + + /** + * Adds a mapping + * + * @param int $generatedLine The line number in generated file + * @param int $generatedColumn The column number in generated file + * @param int $originalLine The line number in original file + * @param int $originalColumn The column number in original file + * @param string $sourceFile The original source file + * + * @return void + */ + public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile) + { + $this->mappings[] = [ + 'generated_line' => $generatedLine, + 'generated_column' => $generatedColumn, + 'original_line' => $originalLine, + 'original_column' => $originalColumn, + 'source_file' => $sourceFile + ]; + + $this->sources[$sourceFile] = $sourceFile; + } + + /** + * Saves the source map to a file + * + * @param string $content The content to write + * + * @return string|null + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved + * @deprecated + */ + public function saveMap($content) + { + $file = $this->options['sourceMapWriteTo']; + assert($file !== null); + $dir = \dirname($file); + + // directory does not exist + if (! is_dir($dir)) { + // FIXME: create the dir automatically? + throw new CompilerException( + sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir) + ); + } + + // FIXME: proper saving, with dir write check! + if (file_put_contents($file, $content) === false) { + throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file)); + } + + return $this->options['sourceMapURL']; + } + + /** + * Generates the JSON source map + * + * @param string $prefix A prefix added in the output file, which needs to shift mappings + * + * @return string + * + * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit# + */ + public function generateJson($prefix = '') + { + $sourceMap = []; + $mappings = $this->generateMappings($prefix); + + // File version (always the first entry in the object) and must be a positive integer. + $sourceMap['version'] = self::VERSION; + + // An optional name of the generated code that this source map is associated with. + $file = $this->options['sourceMapFilename']; + + if ($file) { + $sourceMap['file'] = $file; + } + + // An optional source root, useful for relocating source files on a server or removing repeated values in the + // 'sources' entry. This value is prepended to the individual entries in the 'source' field. + $root = $this->options['sourceRoot']; + + if ($root) { + $sourceMap['sourceRoot'] = $root; + } + + // A list of original sources used by the 'mappings' entry. + $sourceMap['sources'] = []; + + foreach ($this->sources as $sourceFilename) { + $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename); + } + + // A list of symbol names used by the 'mappings' entry. + $sourceMap['names'] = []; + + // A string with the encoded mapping data. + $sourceMap['mappings'] = $mappings; + + if ($this->options['outputSourceFiles']) { + // An optional list of source content, useful when the 'source' can't be hosted. + // The contents are listed in the same order as the sources above. + // 'null' may be used if some original sources should be retrieved by name. + $sourceMap['sourcesContent'] = $this->getSourcesContent(); + } + + // less.js compat fixes + if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) { + unset($sourceMap['sourceRoot']); + } + + $jsonSourceMap = json_encode($sourceMap, JSON_UNESCAPED_SLASHES); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException(json_last_error_msg()); + } + + assert($jsonSourceMap !== false); + + return $jsonSourceMap; + } + + /** + * Returns the sources contents + * + * @return string[]|null + */ + protected function getSourcesContent() + { + if (empty($this->sources)) { + return null; + } + + $content = []; + + foreach ($this->sources as $sourceFile) { + $content[] = file_get_contents($sourceFile); + } + + return $content; + } + + /** + * Generates the mappings string + * + * @param string $prefix A prefix added in the output file, which needs to shift mappings + * + * @return string + */ + public function generateMappings($prefix = '') + { + if (! \count($this->mappings)) { + return ''; + } + + $prefixLines = substr_count($prefix, "\n"); + $lastPrefixNewLine = strrpos($prefix, "\n"); + $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1; + $prefixColumn = strlen($prefix) - $lastPrefixLineStart; + + $this->sourceKeys = array_flip(array_keys($this->sources)); + + // group mappings by generated line number. + $groupedMap = $groupedMapEncoded = []; + + foreach ($this->mappings as $m) { + $groupedMap[$m['generated_line']][] = $m; + } + + ksort($groupedMap); + + $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0; + + foreach ($groupedMap as $lineNumber => $lineMap) { + if ($lineNumber > 1) { + // The prefix only impacts the column for the first line of the original output + $prefixColumn = 0; + } + $lineNumber += $prefixLines; + + while (++$lastGeneratedLine < $lineNumber) { + $groupedMapEncoded[] = ';'; + } + + $lineMapEncoded = []; + $lastGeneratedColumn = 0; + + foreach ($lineMap as $m) { + $generatedColumn = $m['generated_column'] + $prefixColumn; + + $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn); + $lastGeneratedColumn = $generatedColumn; + + // find the index + if ($m['source_file']) { + $index = $this->findFileIndex($m['source_file']); + + if ($index !== false) { + $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex); + $lastOriginalIndex = $index; + // lines are stored 0-based in SourceMap spec version 3 + $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine); + $lastOriginalLine = $m['original_line'] - 1; + $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn); + $lastOriginalColumn = $m['original_column']; + } + } + + $lineMapEncoded[] = $mapEncoded; + } + + $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';'; + } + + return rtrim(implode($groupedMapEncoded), ';'); + } + + /** + * Finds the index for the filename + * + * @param string $filename + * + * @return int|false + */ + protected function findFileIndex($filename) + { + return $this->sourceKeys[$filename]; + } + + /** + * Normalize filename + * + * @param string $filename + * + * @return string + */ + protected function normalizeFilename($filename) + { + $filename = $this->fixWindowsPath($filename); + $rootpath = $this->options['sourceMapRootpath']; + $basePath = $this->options['sourceMapBasepath']; + + // "Trim" the 'sourceMapBasepath' from the output filename. + if (\strlen($basePath) && strpos($filename, $basePath) === 0) { + $filename = substr($filename, \strlen($basePath)); + } + + // Remove extra leading path separators. + if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) { + $filename = substr($filename, 1); + } + + return $rootpath . $filename; + } + + /** + * Fix windows paths + * + * @param string $path + * @param bool $addEndSlash + * + * @return string + */ + public function fixWindowsPath($path, $addEndSlash = false) + { + $slash = ($addEndSlash) ? '/' : ''; + + if (! empty($path)) { + $path = str_replace('\\', '/', $path); + $path = rtrim($path, '/') . $slash; + } + + return $path; + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Type.php b/plugins/admin/vendor/scssphp/scssphp/src/Type.php new file mode 100644 index 0000000..d430887 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Type.php @@ -0,0 +1,208 @@ + + */ +class Type +{ + /** + * @internal + */ + const T_ASSIGN = 'assign'; + /** + * @internal + */ + const T_AT_ROOT = 'at-root'; + /** + * @internal + */ + const T_BLOCK = 'block'; + /** + * @deprecated + * @internal + */ + const T_BREAK = 'break'; + /** + * @internal + */ + const T_CHARSET = 'charset'; + const T_COLOR = 'color'; + /** + * @internal + */ + const T_COMMENT = 'comment'; + /** + * @deprecated + * @internal + */ + const T_CONTINUE = 'continue'; + /** + * @deprecated + * @internal + */ + const T_CONTROL = 'control'; + /** + * @internal + */ + const T_CUSTOM_PROPERTY = 'custom'; + /** + * @internal + */ + const T_DEBUG = 'debug'; + /** + * @internal + */ + const T_DIRECTIVE = 'directive'; + /** + * @internal + */ + const T_EACH = 'each'; + /** + * @internal + */ + const T_ELSE = 'else'; + /** + * @internal + */ + const T_ELSEIF = 'elseif'; + /** + * @internal + */ + const T_ERROR = 'error'; + /** + * @internal + */ + const T_EXPRESSION = 'exp'; + /** + * @internal + */ + const T_EXTEND = 'extend'; + /** + * @internal + */ + const T_FOR = 'for'; + const T_FUNCTION = 'function'; + /** + * @internal + */ + const T_FUNCTION_REFERENCE = 'function-reference'; + /** + * @internal + */ + const T_FUNCTION_CALL = 'fncall'; + /** + * @internal + */ + const T_HSL = 'hsl'; + /** + * @internal + */ + const T_HWB = 'hwb'; + /** + * @internal + */ + const T_IF = 'if'; + /** + * @internal + */ + const T_IMPORT = 'import'; + /** + * @internal + */ + const T_INCLUDE = 'include'; + /** + * @internal + */ + const T_INTERPOLATE = 'interpolate'; + /** + * @internal + */ + const T_INTERPOLATED = 'interpolated'; + /** + * @internal + */ + const T_KEYWORD = 'keyword'; + const T_LIST = 'list'; + const T_MAP = 'map'; + /** + * @internal + */ + const T_MEDIA = 'media'; + /** + * @internal + */ + const T_MEDIA_EXPRESSION = 'mediaExp'; + /** + * @internal + */ + const T_MEDIA_TYPE = 'mediaType'; + /** + * @internal + */ + const T_MEDIA_VALUE = 'mediaValue'; + /** + * @internal + */ + const T_MIXIN = 'mixin'; + /** + * @internal + */ + const T_MIXIN_CONTENT = 'mixin_content'; + /** + * @internal + */ + const T_NESTED_PROPERTY = 'nestedprop'; + /** + * @internal + */ + const T_NOT = 'not'; + const T_NULL = 'null'; + const T_NUMBER = 'number'; + /** + * @internal + */ + const T_RETURN = 'return'; + /** + * @internal + */ + const T_ROOT = 'root'; + /** + * @internal + */ + const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once'; + /** + * @internal + */ + const T_SELF = 'self'; + const T_STRING = 'string'; + /** + * @internal + */ + const T_UNARY = 'unary'; + /** + * @internal + */ + const T_VARIABLE = 'var'; + /** + * @internal + */ + const T_WARN = 'warn'; + /** + * @internal + */ + const T_WHILE = 'while'; +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Util.php b/plugins/admin/vendor/scssphp/scssphp/src/Util.php new file mode 100644 index 0000000..ad608ce --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Util.php @@ -0,0 +1,184 @@ + + * + * @internal + */ +class Util +{ + /** + * Asserts that `value` falls within `range` (inclusive), leaving + * room for slight floating-point errors. + * + * @param string $name The name of the value. Used in the error message. + * @param Range $range Range of values. + * @param array|Number $value The value to check. + * @param string $unit The unit of the value. Used in error reporting. + * + * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin. + * + * @throws \ScssPhp\ScssPhp\Exception\RangeException + */ + public static function checkRange($name, Range $range, $value, $unit = '') + { + $val = $value[1]; + $grace = new Range(-0.00001, 0.00001); + + if (! \is_numeric($val)) { + throw new RangeException("$name {$val} is not a number."); + } + + if ($range->includes($val)) { + return $val; + } + + if ($grace->includes($val - $range->first)) { + return $range->first; + } + + if ($grace->includes($val - $range->last)) { + return $range->last; + } + + throw new RangeException("$name {$val} must be between {$range->first} and {$range->last}$unit"); + } + + /** + * Encode URI component + * + * @param string $string + * + * @return string + */ + public static function encodeURIComponent($string) + { + $revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')']; + + return strtr(rawurlencode($string), $revert); + } + + /** + * mb_chr() wrapper + * + * @param int $code + * + * @return string + */ + public static function mbChr($code) + { + // Use the native implementation if available, but not on PHP 7.2 as mb_chr(0) is buggy there + if (\PHP_VERSION_ID > 70300 && \function_exists('mb_chr')) { + return mb_chr($code, 'UTF-8'); + } + + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6) . \chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12) . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18) . \chr(0x80 | $code >> 12 & 0x3F) + . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F); + } + + return $s; + } + + /** + * mb_strlen() wrapper + * + * @param string $string + * @return int + */ + public static function mbStrlen($string) + { + // Use the native implementation if available. + if (\function_exists('mb_strlen')) { + return mb_strlen($string, 'UTF-8'); + } + + if (\function_exists('iconv_strlen')) { + return (int) @iconv_strlen($string, 'UTF-8'); + } + + throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.'); + } + + /** + * mb_substr() wrapper + * @param string $string + * @param int $start + * @param null|int $length + * @return string + */ + public static function mbSubstr($string, $start, $length = null) + { + // Use the native implementation if available. + if (\function_exists('mb_substr')) { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + if (\function_exists('iconv_substr')) { + if ($start < 0) { + $start = static::mbStrlen($string) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = static::mbStrlen($string) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string)iconv_substr($string, $start, $length, 'UTF-8'); + } + + throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.'); + } + + /** + * mb_strpos wrapper + * @param string $haystack + * @param string $needle + * @param int $offset + * + * @return int|false + */ + public static function mbStrpos($haystack, $needle, $offset = 0) + { + if (\function_exists('mb_strpos')) { + return mb_strpos($haystack, $needle, $offset, 'UTF-8'); + } + + if (\function_exists('iconv_strpos')) { + return iconv_strpos($haystack, $needle, $offset, 'UTF-8'); + } + + throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.'); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php b/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php new file mode 100644 index 0000000..f399e41 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php @@ -0,0 +1,77 @@ +parseValue($source, $value)) { + throw new \InvalidArgumentException(sprintf('Invalid value source "%s".', $source)); + } + + return $value; + } + + /** + * Converts a PHP value to a Sass value + * + * The returned value is guaranteed to be supported by the + * Compiler methods for registering custom variables. No other + * guarantee about it is provided. It should be considered + * opaque values by the caller. + * + * @param mixed $value + * + * @return mixed + */ + public static function fromPhp($value) + { + if ($value instanceof Number) { + return $value; + } + + if (is_array($value) && isset($value[0]) && \in_array($value[0], [Type::T_NULL, Type::T_COLOR, Type::T_KEYWORD, Type::T_LIST, Type::T_MAP, Type::T_STRING])) { + return $value; + } + + if ($value === null) { + return Compiler::$null; + } + + if ($value === true) { + return Compiler::$true; + } + + if ($value === false) { + return Compiler::$false; + } + + if ($value === '') { + return Compiler::$emptyString; + } + + if (\is_int($value) || \is_float($value)) { + return new Number($value, ''); + } + + if (\is_string($value)) { + return [Type::T_STRING, '"', [$value]]; + } + + throw new \InvalidArgumentException(sprintf('Cannot convert the value of type "%s" to a Sass value.', gettype($value))); + } +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Version.php b/plugins/admin/vendor/scssphp/scssphp/src/Version.php new file mode 100644 index 0000000..d604a50 --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Version.php @@ -0,0 +1,23 @@ + + */ +class Version +{ + const VERSION = '1.11.0'; +} diff --git a/plugins/admin/vendor/scssphp/scssphp/src/Warn.php b/plugins/admin/vendor/scssphp/scssphp/src/Warn.php new file mode 100644 index 0000000..592b44c --- /dev/null +++ b/plugins/admin/vendor/scssphp/scssphp/src/Warn.php @@ -0,0 +1,84 @@ + NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Error](https://github.com/getgrav/grav-plugin-error) and [Problems](https://github.com/getgrav/grav-plugin-problems) to operate. + +## Configuration + +Before configuring this plugin, you should copy the `user/plugins/auto-author/auto-author.yaml` to `user/config/plugins/auto-author.yaml` and only edit that copy. + +Here is the default configuration and an explanation of available options: + +```yaml +# enables or disables the plugin +enabled: true +# true = use current admin user (fullname) or false = use site author name (default) +user: false +``` + +## License + +The MIT License (MIT) + +Copyright (c) 2015 Sven Sanzenbacher + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/auto-author/auto-author.php b/plugins/auto-author/auto-author.php new file mode 100755 index 0000000..e751fe6 --- /dev/null +++ b/plugins/auto-author/auto-author.php @@ -0,0 +1,54 @@ + ['onAdminCreatePageFrontmatter', 0] + ]; + } + + /** + * Initialize the plugin + * + * @param Event $event + */ + public function onAdminCreatePageFrontmatter(Event $event) + { + $header = $event['header']; + if (!isset($header['author'])) { + if ($this->config->get('plugins.auto-author.user')) { + /** + * @var Admin $admin + */ + $admin = $this->grav['admin']; + $author = $admin->user->get('fullname'); + + } else { + $author = $this->grav['config']->get('site.author.name'); + } + $header['author'] = $author; + $event['header'] = $header; + } + } +} diff --git a/plugins/auto-author/auto-author.yaml b/plugins/auto-author/auto-author.yaml new file mode 100755 index 0000000..1eaead7 --- /dev/null +++ b/plugins/auto-author/auto-author.yaml @@ -0,0 +1,2 @@ +enabled: true +user: false diff --git a/plugins/auto-author/blueprints.yaml b/plugins/auto-author/blueprints.yaml new file mode 100755 index 0000000..34aff83 --- /dev/null +++ b/plugins/auto-author/blueprints.yaml @@ -0,0 +1,39 @@ +name: Auto Author +version: 0.1.0 +description: Automatically adds an author to frontmatter when creating a new page via Grav Admin plugin +icon: user-circle +author: + name: Sven Sanzenbacher +homepage: https://github.com/naucon/grav-plugin-auto-author +keywords: grav, plugin, auto-author, author, autor, frontmatter +bugs: https://github.com/naucon/grav-plugin-auto-author/issues +docs: https://github.com/naucon/grav-plugin-auto-author/blob/develop/README.md +license: MIT + +dependencies: + - { name: admin, version: '>=1.2.3' } + +form: + validation: strict + fields: + enabled: + type: toggle + label: Plugin status + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + user: + type: toggle + label: Current user + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + help: use current admin user (fullname) or site author diff --git a/plugins/auto-date/CHANGELOG.md b/plugins/auto-date/CHANGELOG.md new file mode 100755 index 0000000..770878e --- /dev/null +++ b/plugins/auto-date/CHANGELOG.md @@ -0,0 +1,17 @@ +# v1.0.2 +## 05/29/2016 + +1. [](#bugfix) + * Fixed links in blueprint + +# v1.0.1 +## 09/30/2016 + +1. [](#improved) + * Fixed icon + +# v1.0.0 +## 09/30/2016 + +1. [](#new) + * Initial Release diff --git a/plugins/auto-date/LICENSE b/plugins/auto-date/LICENSE new file mode 100755 index 0000000..6c7da64 --- /dev/null +++ b/plugins/auto-date/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Team Grav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/auto-date/README.md b/plugins/auto-date/README.md new file mode 100755 index 0000000..4d5d46b --- /dev/null +++ b/plugins/auto-date/README.md @@ -0,0 +1,7 @@ +# Auto Date Plugin + +The **Auto Date** Plugin is designed for [Grav CMS](http://github.com/getgrav/grav) and automatically adds the current date to frontmatter when createing a new page via the Admin plugin. + +## Description + +There's not much to this plugin, simply install it and whenever you create a new page via the admin plugin, the current date will be inserted in the frontmatter of the page and show up in the **Options** -> **Publishing** Tab. The format of the date is dependent upon the value set in `system.yaml` under `pages: dateformat: default` and if not set will use `H:i d-m-Y` as the date format. diff --git a/plugins/auto-date/auto-date.php b/plugins/auto-date/auto-date.php new file mode 100755 index 0000000..cdc7b9e --- /dev/null +++ b/plugins/auto-date/auto-date.php @@ -0,0 +1,42 @@ + ['onAdminCreatePageFrontmatter', 0] + ]; + } + + /** + * Initialize the plugin + */ + public function onAdminCreatePageFrontmatter(Event $event) + { + $header = $event['header']; + if (!isset($header['date'])) { + $header['date'] = date($this->grav['config']->get('system.pages.dateformat.default', 'H:i d-m-Y')); + $header['publish_date'] = date($this->grav['config']->get('system.pages.dateformat.default', 'H:i d-m-Y')); + $event['header'] = $header; + } + } +} diff --git a/plugins/auto-date/auto-date.yaml b/plugins/auto-date/auto-date.yaml new file mode 100755 index 0000000..d4ca941 --- /dev/null +++ b/plugins/auto-date/auto-date.yaml @@ -0,0 +1 @@ +enabled: true diff --git a/plugins/auto-date/blueprints.yaml b/plugins/auto-date/blueprints.yaml new file mode 100755 index 0000000..2988d51 --- /dev/null +++ b/plugins/auto-date/blueprints.yaml @@ -0,0 +1,30 @@ +name: Auto Date +version: 1.0.2 +description: Automatically adds date to frontmatter when creating a new page via Grav Admin plugin +icon: clock-o +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +homepage: https://github.com/getgrav/grav-plugin-auto-date +keywords: plugin, auto-date, date, frontmatter +bugs: https://github.com/getgrav/grav-plugin-auto-date/issues +docs: https://github.com/getgrav/grav-plugin-auto-date/blob/develop/README.md +license: MIT + +dependencies: + - { name: admin, version: '>=1.2.3' } + +form: + validation: strict + fields: + enabled: + type: toggle + label: Plugin status + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool diff --git a/plugins/autoseo/.gitignore b/plugins/autoseo/.gitignore new file mode 100755 index 0000000..6c224e0 --- /dev/null +++ b/plugins/autoseo/.gitignore @@ -0,0 +1,41 @@ +# Cache and logs (Symfony2) +/app/cache/* +/app/logs/* +!app/cache/.gitkeep +!app/logs/.gitkeep + +# Email spool folder +/app/spool/* + +# Cache, session files and logs (Symfony3) +/var/cache/* +/var/logs/* +/var/sessions/* +!var/cache/.gitkeep +!var/logs/.gitkeep +!var/sessions/.gitkeep + +# Parameters +/app/config/parameters.yml +/app/config/parameters.ini + +# Managed by Composer +/app/bootstrap.php.cache +/var/bootstrap.php.cache +/bin/* +!bin/console +!bin/symfony_requirements + +# Assets and user uploads +/web/bundles/ +/web/uploads/ + +# PHPUnit +/app/phpunit.xml +/phpunit.xml + +# Build data +/build/ + +# Backup entities generated with doctrine:generate:entities command +**/Entity/*~ diff --git a/plugins/autoseo/CHANGELOG.md b/plugins/autoseo/CHANGELOG.md new file mode 100755 index 0000000..9c05251 --- /dev/null +++ b/plugins/autoseo/CHANGELOG.md @@ -0,0 +1,134 @@ +# v1.4.0 +## 04/08/2022 + +1. []() + * takeover release +2. [](#bugfix) + * do not crash on mbregex search failure in php_mbereg_replace_exec. + +# v1.3.5 +## 01/26/2018 + +1. [](#bugfix) + * fix release dates in changelog. + +# v1.3.3 +## 01/26/2018 + +1. [](#bugfix) + * replace meta title content by page title instead of page slug. + +# v1.3.2 +## 01/10/2018 + +1. [](#bugfix) + * The meta tag fields were wrongly named "property" instead of "name". + +# v1.3.1 +## 08/03/2017 + +1. [](#improved) + * Add Admin plugin dependency (Dávid Szabó ) + * Use Slug utility function (Dávid Szabó +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Check you satisfy the Grav requirements** — [http://learn.getgrav.org/basics/requirements](http://learn.getgrav.org/basics/requirements) + +1. **Check this happens on a new Plugin install** — remove and re-install the plugin. Reset the plugin to its default configuration. + +1. **Check this happens on a clean Grav install** — check if the issue happens on any Grav site, or just with a specific configuration of plugins / theme + +1. **Use the GitHub issue search** — check if the issue has already been + reported. + +1. **Check if the issue is already being solved in a PR** — check the open Pull Requests to see if one already solves the problem you're having + +1. **Check if the issue has been fixed** — try to reproduce it using the + latest `develop` branch in the repository. + +1. **Isolate the problem** — create a [reduced test + case](http://css-tricks.com/reduced-test-cases/) and provide a step-by-step instruction set on how to recreate the problem. Include code samples, page snippets or yaml configurations if needed. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. + +What is your environment? Is it localhost, OSX, Linux, on a remote server? Same happening locally and or the server, or just locally or just on Linux? + +What steps will reproduce the issue? What browser(s) and OS experience the problem? + +What would you expect to be the outcome? + +Did the problem start happening recently (e.g. after updating to a new version of this plugin) or was this always a problem? + +If the problem started happening recently, can you reproduce the problem in an older version of this plugin ? What's the most recent version in which the problem doesn't happen? You can download older versions from the releases page on Github. + +Can you reliably reproduce the issue? If not, provide details about how often the problem happens and under which conditions it normally happens. + + +All these details will help people to fix any potential bugs. + +Important: [include Code Samples in triple backticks](https://help.github.com/articles/github-flavored-markdown/#fenced-code-blocks) so that Github will provide a proper indentation. [Add the language name after the backticks](https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting) to add syntax highlighting to the code snippets. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the browser/OS environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +>> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + + + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +Please adhere to the coding conventions used throughout the GRAV project (indentation, +accurate comments, etc.) and any other requirements. +**Please also respect the plugin specific conventions if any.** + +See [Using Pull Request](https://help.github.com/articles/using-pull-requests/) and [Fork a Repo](https://help.github.com/articles/fork-a-repo/) if you're not familiar with Pull Requests. + +Any pull request should be based on the `develop` branch. We will not consider pull requests made to master. + +**IMPORTANT**: By submitting a patch, you agree to allow the project owner to +license your work under the same license as that used by the project. + + +### Translations +Translations for this plugin are managed by direct request. Just send me an mail with your translation file. diff --git a/plugins/autoseo/LICENSE b/plugins/autoseo/LICENSE new file mode 100755 index 0000000..681d44e --- /dev/null +++ b/plugins/autoseo/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Laurent Ongaro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/autoseo/README.md b/plugins/autoseo/README.md new file mode 100755 index 0000000..c784ffb --- /dev/null +++ b/plugins/autoseo/README.md @@ -0,0 +1,114 @@ +# [Grav](http://getgrav.org) AutoSEO + +This is the new home of AutoSEO. +I tried to contact the original author (Laurent), but the email bounced back + + +Grav AutoSEO is a plugin for Grav with which you can fill automatically the description and keywords metadata of a page using its content. +It also adds Facebook Open Graph metadata and Twitter Cards Meta Tags (**New** feature since the 1.2 version). + +If the plugin is enabled and **the description and keyword metadata of the page are not manually filled** (i.e. the page headers contains some metadata fields), +the plugin will use the first words of the page summary to fill the description and its categories and tags to fill the keyword metadata. +Facebook and twitter metadata will also be filled if they are enabled in settings and not manually filled in the page header. + +## Installation + +### Manual Installation + +To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `autoseo`. + +You should now have all the plugin files under + +`/your/site/grav/user/plugins/autoseo` + +## Configuration + +Here is the default configuration and an explanation of available options: + +`autoseo.yaml:` + +```yaml +enabled: true # lets you turn the plugin off and on. +description: + enabled: true # lets you turn the plugin off and on for the "description" metadata. + length: 30 # maximal number of words that will be used to fill the "description" metadata. +keywords: + enabled: true # lets you turn the plugin off and on for the "keywords" metadata. + length: 20 # maximal number of words that will be used to fill the "keywords" metadata. +facebook: + enabled: true # Lets you turn the plugin ON and OFF for the "Facebook Open Graph" metadata. +twitter: + enabled: true # Lets you turn the plugin ON and OFF for the "Twitter Cards" metadata. +``` + + * The first `enabled` field lets you turn the plugin on and off. + * The `enabled` field in each metadata section lets you turn on and off the plugin for this metadata only. + * `length` is the maximal count of words that will be used to fill the corresponding metadata. + +If you need to change any value, then the best process is to copy the `autoseo.yaml` file into your `users/config/plugins/` folder (create it if it doesn't exist), and then modify there. This will override the default settings. + +## Per-Page Configuration + +If you want to alter the settings for one or a few pages only, you can do so by adding page specific configurations into your page headers, e.g. + +```yaml +autoseo: false +``` +to disable the `AutoSeo` plugin just for this page. + +Remember you can also override the general plugin settings for a specific page by adding settings in its header. For instance, adding the following lines in a page headers + +```yaml +autoseo: + enabled: true + description: + enabled: true + length: 10 + keywords: + enabled: true + length: 10 + facebook: + enabled: true + twitter: + enabled: false +``` + +will change the description and keyword lengths to 10 words for these page only, enable Facebook Open Graph metadata and disable Twitter Cards tags. + +## Usage + +There's not much to do with this plugin, simply install, enabled it and check its configuration to meet your needs. + +No need to do anything else. + +### Demo + +The plugin is installed and enabled on the following blog : [http://www.gamecoderblog.com](http://www.gamecoderblog.com). + +To see it in action, you have to inspect the meta of the page code source. +**(Use a page other than the home page, because the metadata used in it are those set in the blog configuration)** + +### Performances + +As each page must be analyzed and its content filtered by the plugins, the impact on performance can be important when first access to a page. +But as the result will be cached, once the first analysis is done, you will find no further negative performance impact. + +To limit the impact on performance, **the content analyzed will always be truncated to the 1000 first characters for description metadata**, then the limit given by the `length` setting will be applied. + +### Notes + +`AutoSeo` will make a few cleaning but won't check the content used to auto fill the metadata. +So be sure that the first words of your content (the summary) will be correct to be used as a description. +In a same way, check that the tags and categories of the page are OK to be used as keyword. + +### Bugs And TODO + +Please send any comments or bug reports to the plugin's issue tracker. + +#### TODO + +* No new feature is planned for the moment. + +### Authors + +`AutoSeo` was developed and maintained by Laurent Ongaro and is now looked after by Nuno Costa. diff --git a/plugins/autoseo/autoseo.php b/plugins/autoseo/autoseo.php new file mode 100755 index 0000000..a8331af --- /dev/null +++ b/plugins/autoseo/autoseo.php @@ -0,0 +1,247 @@ + ['onPluginsInitialized', 0], + ]; + } + /** + * Register events with Grav + * @return void + */ + public function onPluginsInitialized() + { + // deactivate plugin in admin + if ( !$this->isAdmin() + and $this->config->get('plugins.autoseo.enabled') + ) { + $this->enable([ + 'onPageInitialized' => ['onPageInitialized', 0] + ]); + } + } + + /** + * Add content after page content was read into the system. + * + */ + public function onPageInitialized() + { + $page = $this->grav['page']; + $config = $this->mergeConfig($page); + if ( !$config['enabled']) return; + + $updateDescription = $config['description.enabled']; + $updatekeywords = $config['keywords.enabled']; + $updateFacebook = $config['facebook.enabled']; + $updateTwitter = $config['twitter.enabled']; + if (!$updateDescription && !$updatekeywords && !$updateFacebook && !$updateTwitter) return; + + $meta = $page->metadata(); + + $metaSite = $this->config->get('site')['metadata']; + + // limit the content size to reduce the performance impact + $content = mb_substr(strip_tags($page->content()),0, 1000 ); + + $cleanContent = $this->cleanText ($content, $config); // here because we don't want to make this call several times + $cleanTitle = $this->cleanString ($page->title()); // here because we don't want to make this call several times + + if ($updateDescription) $meta = $this->getMetaDescription ($meta, $metaSite, $config, $cleanContent); + if ($updatekeywords) $meta = $this->getMetaKeywords ($meta, $metaSite, $config); + if ($updateFacebook) $meta = $this->getMetaOpenGraph ($meta, $metaSite, $config, $cleanContent, $cleanTitle); + if ($updateTwitter) $meta = $this->getMetaTwitter ($meta, $metaSite, $config, $cleanContent, $cleanTitle); + $page->metadata ($meta); + } + + // PROCESS for the description metadata + private function getMetaDescription ($meta, $metaSite, $config, $cleanContent) { + + if (array_key_exists('description', $metaSite)) { $metaSiteContent = htmlspecialchars($metaSite['description'], ENT_QUOTES, 'UTF-8'); } else { $metaSiteContent = ''; } + // if the page has a meta that is different from the default one, we return its value + if (!empty($meta['description']['content']) && $meta['description']['content'] != $metaSiteContent) return $meta; + + $metaPageContent = $cleanContent; + + if (empty($metaPageContent)) $metaPageContent = $metaSiteContent; + + $meta['description'] = [ 'name' => 'description', 'content' => $metaPageContent]; + return $meta; + } + + // PROCESS for the keywords metadata + private function getMetaKeywords ($meta, $metaSite, $config) { + $page = $this->grav['page']; + + if (array_key_exists('keywords', $metaSite)) { $metaSiteContent = htmlspecialchars($metaSite['keywords'], ENT_QUOTES, 'UTF-8'); } else { $metaSiteContent = ''; } + + // if the page has a meta that is different from the default one, we return its value + if (!empty($meta['keywords']['content']) && $meta['keywords']['content'] != $metaSiteContent) return $meta; + $length = $config['keywords.length']; + if ($length <=1 ) $length=20; + + // we create a keywords list using the page tags and categories + if (array_key_exists( 'category', $page->taxonomy() )) { $categories = $page->taxonomy()['category']; } else { $categories = []; } + if (array_key_exists( 'tag', $page->taxonomy() )) { $tags = $page->taxonomy()['tag']; } else { $tags = []; } + + $content = array_merge ($categories, $tags) ; + $content = array_unique ($content); + $content = array_slice($content, 0, $length); + $content = join(',',$content); + $content = $this->cleanString($content); + $metaPageContent = $content; + if (empty($metaPageContent)) $metaPageContent = $metaSiteContent; + + $meta['keywords'] = [ 'name' => 'keywords', 'content' => $metaPageContent]; + return $meta; + } + + // PROCESS for the OpenGraph metadata + private function getMetaOpenGraph ($meta, $metaSite, $config, $cleanContent, $cleanTitle) { + $page = $this->grav['page']; + + $meta['og:sitename']['name'] = 'og:sitename'; + $meta['og:sitename']['property'] = 'og:sitename'; + $meta['og:sitename']['content'] = $this->config->get('site.title'); + $meta['og:title']['name'] = 'og:title'; + $meta['og:title']['property'] = 'og:title'; + $meta['og:title']['content'] = $cleanTitle; + $meta['og:type']['name'] = 'og:type'; + $meta['og:type']['property'] = 'og:type'; + $meta['og:type']['content'] = 'article'; + $meta['og:url']['name'] = 'og:url'; + $meta['og:url']['property'] = 'og:url'; + $meta['og:url']['content'] = $this->grav['uri']->url(true); + $meta['og:description']['name'] = 'og:description'; + $meta['og:description']['property'] = 'og:description'; + if (empty($cleanContent)) $cleanContent = $meta['description']['content']; + else { + if (array_key_exists('description', $metaSite)) { $metaSiteContent = htmlspecialchars($metaSite['description'], ENT_QUOTES, 'UTF-8'); } else { $metaSiteContent = ''; } + if ($meta['description']['content'] != $metaSiteContent) $cleanContent = $meta['description']['content']; + } + + $meta['og:description']['content'] = $cleanContent; + + if (!empty($page->value('media.image'))) { + $images = $page->media()->images(); + $image = array_shift($images); + $meta['og:image']['name'] = 'og:image'; + $meta['og:image']['property'] = 'og:image'; + $meta['og:image']['content'] = $this->grav['uri']->base() . $image->url(); + } + return $meta; + } + + // PROCESS for the twitter metadata + private function getMetaTwitter ($meta, $metaSite, $config, $cleanContent, $cleanTitle) { + $page = $this->grav['page']; + + if (!isset($meta['twitter:card'])) { + $meta['twitter:card']['name'] = 'twitter:card'; + $meta['twitter:card']['property'] = 'twitter:card'; + $meta['twitter:card']['content'] = 'summary_large_image'; + } + + if (!isset($meta['twitter:title'])) { + $meta['twitter:title']['name'] = 'twitter:title'; + $meta['twitter:title']['property'] = 'twitter:title'; + $meta['twitter:title']['content'] = $cleanTitle; + } + + if (!isset($meta['twitter:description'])) { + $meta['twitter:description']['name'] = 'twitter:description'; + $meta['twitter:description']['property'] = 'twitter:description'; + if (empty($cleanContent)) + $cleanContent = $meta['description']['content']; + else { + if (array_key_exists('description', $metaSite)) { $metaSiteContent = htmlspecialchars($metaSite['description'], ENT_QUOTES, 'UTF-8'); } else { $metaSiteContent = ''; } + if ($meta['description']['content'] != $metaSiteContent) $cleanContent = $meta['description']['content']; + } + $meta['twitter:description']['content'] = mb_substr($cleanContent,0,140); + } + + if (!isset($meta['twitter:image'])) { + if (!empty($page->value('media.image'))) { + $images = $page->media()->images(); + $image = array_shift($images); + $meta['twitter:image']['name'] = 'twitter:image'; + $meta['twitter:image']['property'] = 'twitter:image'; + $meta['twitter:image']['content'] = $this->grav['uri']->base() . $image->url(); + } + } + return $meta; + } + + private function cleanMarkdown($text){ + $rules = array ( + '/(#+)(.*)/' => '\2', // headers + '/(<|<)!--\n((.*|\n)*)\n--(>|\>)/' => '', // comments + '/(\*|-|_){3}/' => '', // hr + '/!\[([^\[]+)\]\(([^\)]+)\)/' => '', // images + '/\[([^\[]+)\]\(([^\)]+)\)/' => '\1', // links + '/(\*\*|__)(.*?)\1/' => '\2', // bold + '/(\*|_)(.*?)\1/' => '\2', // emphasis + '/\~\~(.*?)\~\~/' => '\1', // del + '/\:\"(.*?)\"\:/' => '\1', // quote + '/```(.*)\n((.*|\n)+)\n```/' => '\2', // fence code + '/`(.*?)`/' => '\1', // inline code + '/(\*|\+|-)(.*)/' => '\2', // ul lists + '/\n[0-9]+\.(.*)/' => '\2', // ol lists + '/(>|\>)+(.*)/' => '\2', // blockquotes + ); + foreach ($rules as $regex => $replacement) { + if (is_callable ( $replacement)) { + $text = preg_replace_callback ($regex, $replacement, $text); + } else { + $text = preg_replace ($regex, $replacement, $text); + } + } + $text=str_replace(".\n", '.', $text); + $text=str_replace("\n", '.', $text); + $text=str_replace('"', '', $text); + + return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + } + + private function cleanText ($content, $config) { + try { + + $length = $config['description.length']; + if ($length <=1 ) $length=20; + + $content = $this->cleanMarkdown($content); + // truncate the content to the number of words set in config + $contentSmall = mb_ereg_replace('((\w+\W*){'.$length.'}(\w+))(.*)', '\\1', $content); // beware if content is less than length words, it will be nulled + if ($contentSmall == '' ) $contentSmall = $content; + + return $contentSmall; + } catch (\Throwable $e) { + $this->grav['log']->addError($e->getMessage()); + + return $content; + } + } + + private function cleanString ($content) { + // remove some annoying characters + $content = str_replace(" ",' ',$content); + $content = str_replace('"',"'",$content); + $content = trim($content); + // Removes special chars. + // $content = \Grav\Plugin\Admin\Utils::slug($content); + return $content; + } +} diff --git a/plugins/autoseo/autoseo.yaml b/plugins/autoseo/autoseo.yaml new file mode 100755 index 0000000..0650cb2 --- /dev/null +++ b/plugins/autoseo/autoseo.yaml @@ -0,0 +1,11 @@ +enabled: true # lets you turn the plugin off and on. +description: + enabled: true # lets you turn the plugin off and on for the "description" metadata. + length: 30 # maximal number of words that will be used to fill the "description" metadata. +keywords: + enabled: true # lets you turn the plugin off and on for the "keywords" metadata. + length: 20 # maximal number of words that will be used to fill the "keywords" metadata. +facebook: + enabled: true # Lets you turn the plugin ON and OFF for the "Facebook Open Graph" metadata. +twitter: + enabled: true # Lets you turn the plugin ON and OFF for the "Twitter Cards" metadata. \ No newline at end of file diff --git a/plugins/autoseo/blueprints.yaml b/plugins/autoseo/blueprints.yaml new file mode 100755 index 0000000..d3afc4b --- /dev/null +++ b/plugins/autoseo/blueprints.yaml @@ -0,0 +1,107 @@ +name: Auto SEO +version: 1.4.0 +description: Grav AutoSEO is a plugin for Grav with which you can fill automatically the description and keywords metadata of a page using its content. It also adds Facebook Open Graph metadata and Twitter Cards Meta Tags. +icon: magic +author: + name: Nuno Costa + email: info@codehaha.com + url: http://www.codehaha.com +homepage: https://github.com/francodacosta/grav-plugin-autoseo +keywords: plugin, seo, metadata extraction, auto fill +bugs: https://github.com/francodacosta/grav-plugin-autoseo/issues +docs: https://github.com/francodacosta/grav-plugin-autoseo/blob/master/README.md +license: MIT +dependencies: + - { name: admin, version: '>=1.5.0' } + +form: + validation: strict + fields: + + global: + type: section + title: PLUGINS.AUTO_SEO.GLOBAL_CONFIG + underline: 1 + + fields: + enabled: + type: toggle + label: PLUGINS.AUTO_SEO.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + metadata: + type: section + title: PLUGINS.AUTO_SEO.DEFAULT_CONFIG + underline: 1 + + fields: + description.enabled: + type: toggle + label: PLUGINS.AUTO_SEO.DESCRIPTION.ENABLED_LABEL + help: PLUGINS.AUTO_SEO.DESCRIPTION.ENABLED_HELP + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + description.length: + type: text + size: x-small + label: PLUGINS.AUTO_SEO.DESCRIPTION.LENGTH_LABEL + help: PLUGINS.AUTO_SEO.DESCRIPTION.LENGTH_HELP + default: 30 + validate: + type: number + min: 2 + max: 50 + keywords.enabled: + type: toggle + label: PLUGINS.AUTO_SEO.KEYWORDS.ENABLED_LABEL + help: PLUGINS.AUTO_SEO.KEYWORDS.ENABLED_HELP + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + keywords.length: + type: text + size: x-small + label: PLUGINS.AUTO_SEO.KEYWORDS.LENGTH_LABEL + help: PLUGINS.AUTO_SEO.KEYWORDS.LENGTH_HELP + default: 20 + validate: + type: number + min: 2 + max: 30 + facebook.enabled: + type: toggle + label: PLUGINS.AUTO_SEO.FACEBOOK.ENABLED_LABEL + help: PLUGINS.AUTO_SEO.FACEBOOK.ENABLED_HELP + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + twitter.enabled: + type: toggle + label: PLUGINS.AUTO_SEO.TWITTER.ENABLED_LABEL + help: PLUGINS.AUTO_SEO.TWITTER.ENABLED_HELP + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/autoseo/languages.yaml b/plugins/autoseo/languages.yaml new file mode 100755 index 0000000..9829a9d --- /dev/null +++ b/plugins/autoseo/languages.yaml @@ -0,0 +1,49 @@ +# English +en: + PLUGINS: + AUTO_SEO: + GLOBAL_CONFIG: Global configuration + PLUGIN_STATUS: Plugin status + DEFAULT_CONFIG: Default values for the plugin configuration + PLUGIN_DEPENDENCY_FAILED: Auto SEO Plugin failed to load. Composer dependencies not met. + DESCRIPTION: + ENABLED_LABEL: Auto fill for "description" meta + ENABLED_HELP: Lets you turn the plugin ON and OFF for the "description" metadata only. + LENGTH_LABEL: Maximum word count for "description" meta + LENGTH_HELP: The maximum count of words that will be used to fill the "description" metadata. + KEYWORDS: + ENABLED_LABEL: Auto fill for the "keywords" meta + ENABLED_HELP: Lets you turn the plugin ON and OFF for the "keywords" metadata only. + LENGTH_LABEL: Maximum word count for "keywords" meta. + LENGTH_HELP: The maximum count of words that will be used to fill the "keyword" metadata. + FACEBOOK: + ENABLED_LABEL: Auto fill for the Open Graph tags (facebook) + ENABLED_HELP: Lets you turn the plugin ON and OFF for the "Open Graph" metadata (facebook). + TWITTER: + ENABLED_LABEL: Auto fill for the Twitter tags + ENABLED_HELP: Lets you turn the plugin ON and OFF for the "Twitter Cards" metadata. + +# French +fr: + PLUGINS: + AUTO_SEO: + GLOBAL_CONFIG: Configuration globale + PLUGIN_STATUS: Etat du plugin + DEFAULT_CONFIG: Valeurs par défaut pour la configuration du plugin + PLUGIN_DEPENDENCY_FAILED: le plug-in Auto SEO n'a pas pu être chargé. Les dépendances de Composer ne sont pas remplies. + DESCRIPTION: + ENABLED_LABEL: Remplissage automatique pour la méta "Description" + ENABLED_HELP: Permet de mettre le plugin ON et OFF pour la métadonnée "Description" uniquement. + LENGTH_LABEL: nombre maximal de mots pour la méta "Description" + LENGTH_HELP: le nombre maximum de mots qui seront utilisés pour remplir la métadonnée "Description". + KEYWORDS: + ENABLED_LABEL: Remplissage automatique pour la méta "Keywords" + ENABLED_HELP: Permet de mettre le plugin ON et OFF pour la métadonnée "Keywords" uniquement. + LENGTH_LABEL: Nombre maximal de mots pour la méta "Keywords". + LENGTH_HELP: Le nombre maximum de mots qui seront utilisés pour remplir la métadonnée "Keywords". + FACEBOOK: + ENABLED_LABEL: Remplissage automatique pour les tags Open Graph (facebook) + ENABLED_HELP: Permet de mettre le plugin ON et OFF pour la métadonnée "Open Graph" (facebook). + TWITTER: + ENABLED_LABEL: Remplissage automatique pour les tags Twitter + ENABLED_HELP: Permet de mettre le plugin ON et OFF pour la métadonnée "Twitter Cards". diff --git a/plugins/childlist/CHANGELOG.md b/plugins/childlist/CHANGELOG.md new file mode 100755 index 0000000..75d7e63 --- /dev/null +++ b/plugins/childlist/CHANGELOG.md @@ -0,0 +1,5 @@ +# v0.1.0 +## 07/14/2020 + +1. [](#new) + * ChangeLog started... diff --git a/plugins/childlist/LICENSE b/plugins/childlist/LICENSE new file mode 100755 index 0000000..82f90bb --- /dev/null +++ b/plugins/childlist/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 armansansd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/childlist/README.md b/plugins/childlist/README.md new file mode 100755 index 0000000..cebb79a --- /dev/null +++ b/plugins/childlist/README.md @@ -0,0 +1,56 @@ +# Childlist Plugin + +**This README.md file should be modified to describe the features, installation, configuration, and general usage of the plugin.** + +The **Childlist** Plugin is an extension for [Grav CMS](http://github.com/getgrav/grav). display a list of children page in admin + +## Installation + +Installing the Childlist plugin can be done in one of three ways: The GPM (Grav Package Manager) installation method lets you quickly install the plugin with a simple terminal command, the manual method lets you do so via a zip file, and the admin method lets you do so via the Admin Plugin. + +### GPM Installation (Preferred) + +To install the plugin via the [GPM](http://learn.getgrav.org/advanced/grav-gpm), through your system's terminal (also called the command line), navigate to the root of your Grav-installation, and enter: + + bin/gpm install childlist + +This will install the Childlist plugin into your `/user/plugins`-directory within Grav. Its files can be found under `/your/site/grav/user/plugins/childlist`. + +### Manual Installation + +To install the plugin manually, download the zip-version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `childlist`. You can find these files on [GitHub](https://github.com//grav-plugin-childlist) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/childlist + +> NOTE: This plugin is a modular component for Grav which may require other plugins to operate, please see its [blueprints.yaml-file on GitHub](https://github.com//grav-plugin-childlist/blob/master/blueprints.yaml). + +### Admin Plugin + +If you use the Admin Plugin, you can install the plugin directly by browsing the `Plugins`-menu and clicking on the `Add` button. + +## Configuration + +Before configuring this plugin, you should copy the `user/plugins/childlist/childlist.yaml` to `user/config/plugins/childlist.yaml` and only edit that copy. + +Here is the default configuration and an explanation of available options: + +```yaml +enabled: true +``` + +Note that if you use the Admin Plugin, a file with your configuration named childlist.yaml will be saved in the `user/config/plugins/`-folder once the configuration is saved in the Admin. + +## Usage + +**Describe how to use the plugin.** + +## Credits + +**Did you incorporate third-party code? Want to thank somebody?** + +## To Do + +- [ ] Future plans, if any + diff --git a/plugins/childlist/blueprints.yaml b/plugins/childlist/blueprints.yaml new file mode 100755 index 0000000..7e81a81 --- /dev/null +++ b/plugins/childlist/blueprints.yaml @@ -0,0 +1,36 @@ +name: Childlist +slug: childlist +type: plugin +version: 0.1.0 +description: display a list of children page in admin +icon: plug +author: + name: armansansd + email: contact@armansansd.net +homepage: https://github.com/armansansd/grav-plugin-childlist +demo: http://demo.yoursite.com +keywords: grav, plugin, etc +bugs: https://github.com/armansansd/grav-plugin-childlist/issues +docs: https://github.com/armansansd/grav-plugin-childlist/blob/develop/README.md +license: MIT + +dependencies: + - { name: grav, version: '>=1.6.0' } + +form: + validation: loose + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + text_var: + type: text + label: PLUGIN_CHILDLIST.TEXT_VARIABLE + help: PLUGIN_CHILDLIST.TEXT_VARIABLE_HELP diff --git a/plugins/childlist/childlist.php b/plugins/childlist/childlist.php new file mode 100755 index 0000000..fec98b4 --- /dev/null +++ b/plugins/childlist/childlist.php @@ -0,0 +1,64 @@ + [ + ['autoload', 100000], // TODO: Remove when plugin requires Grav >=1.7 + ['onPluginsInitialized', 0] + ] + ]; + } + + /** + * Composer autoload. + *is + * @return ClassLoader + */ + public function autoload(): ClassLoader + { + return require __DIR__ . '/vendor/autoload.php'; + } + + /** + * Initialize the plugin + */ + public function onPluginsInitialized() + { + // Don't proceed if we are in the admin plugin + // if ($this->isAdmin()) { + // return; + // } + + // Enable the main events we are interested in + $this->enable([ + // Put your main events here + ]); + } + public function onAdminTwigTemplatePaths($event){ + $paths = $event['paths']; + $paths[] = __DIR__ . '/admin/themes/grav/templates'; + $event['paths'] = $paths; + } +} diff --git a/plugins/childlist/childlist.yaml b/plugins/childlist/childlist.yaml new file mode 100755 index 0000000..edbe8cb --- /dev/null +++ b/plugins/childlist/childlist.yaml @@ -0,0 +1,3 @@ +enabled: true +activate: true +text_var: Custom Text added by the **Childlist** plugin (disable plugin to remove) diff --git a/plugins/childlist/composer.json b/plugins/childlist/composer.json new file mode 100755 index 0000000..2888534 --- /dev/null +++ b/plugins/childlist/composer.json @@ -0,0 +1,29 @@ +{ + "name": "armansansd/childlist", + "type": "grav-plugin", + "description": "display a list of children page in admin", + "keywords": ["plugin"], + "homepage": "https://github.com/armansansd/grav-plugin-childlist", + "license": "MIT", + "authors": [ + { + "name": "armansansd", + "email": "contact@armansansd.net", + "role": "Developer" + } + ], + "require": { + "php": ">=7.1.3" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\Childlist\\": "classes/" + }, + "classmap": ["childlist.php"] + }, + "config": { + "platform": { + "php": "7.1.3" + } + } +} diff --git a/plugins/childlist/composer.lock b/plugins/childlist/composer.lock new file mode 100755 index 0000000..405b6d9 --- /dev/null +++ b/plugins/childlist/composer.lock @@ -0,0 +1,23 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "7d150ab33e09ea524591068542a890b2", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.1.3" + }, + "platform-dev": [], + "platform-overrides": { + "php": "7.1.3" + }, + "plugin-api-version": "1.1.0" +} diff --git a/plugins/childlist/languages.yaml b/plugins/childlist/languages.yaml new file mode 100755 index 0000000..7ba9169 --- /dev/null +++ b/plugins/childlist/languages.yaml @@ -0,0 +1,4 @@ +en: + PLUGIN_CHILDLIST: + TEXT_VARIABLE: Text Variable + TEXT_VARIABLE_HELP: Text to add to the top of a page diff --git a/plugins/childlist/template/form/fields/childlist/childlist.html.twig b/plugins/childlist/template/form/fields/childlist/childlist.html.twig new file mode 100755 index 0000000..3b04284 --- /dev/null +++ b/plugins/childlist/template/form/fields/childlist/childlist.html.twig @@ -0,0 +1,9 @@ +{% extends "forms/field.html.twig" %} + +{% block input_attributes %} + type="range" + {% if field.validate.min %}min="{{ field.validate.min }}"{% endif %} + {% if field.validate.max %}max="{{ field.validate.max }}"{% endif %} + {% if field.validate.step %}step="{{ field.validate.step }}"{% endif %} + {{ parent() }} +{% endblock %} diff --git a/plugins/childlist/vendor/autoload.php b/plugins/childlist/vendor/autoload.php new file mode 100755 index 0000000..0c74a6d --- /dev/null +++ b/plugins/childlist/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/plugins/childlist/vendor/composer/LICENSE b/plugins/childlist/vendor/composer/LICENSE new file mode 100755 index 0000000..f27399a --- /dev/null +++ b/plugins/childlist/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/childlist/vendor/composer/autoload_classmap.php b/plugins/childlist/vendor/composer/autoload_classmap.php new file mode 100755 index 0000000..7131927 --- /dev/null +++ b/plugins/childlist/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $baseDir . '/childlist.php', +); diff --git a/plugins/childlist/vendor/composer/autoload_namespaces.php b/plugins/childlist/vendor/composer/autoload_namespaces.php new file mode 100755 index 0000000..b7fc012 --- /dev/null +++ b/plugins/childlist/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/classes'), +); diff --git a/plugins/childlist/vendor/composer/autoload_real.php b/plugins/childlist/vendor/composer/autoload_real.php new file mode 100755 index 0000000..ff5b708 --- /dev/null +++ b/plugins/childlist/vendor/composer/autoload_real.php @@ -0,0 +1,55 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit910efd51a496a8e9d2c9ce9e3608936d::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/plugins/childlist/vendor/composer/autoload_static.php b/plugins/childlist/vendor/composer/autoload_static.php new file mode 100755 index 0000000..a100b23 --- /dev/null +++ b/plugins/childlist/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'Grav\\Plugin\\Childlist\\' => 22, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Grav\\Plugin\\Childlist\\' => + array ( + 0 => __DIR__ . '/../..' . '/classes', + ), + ); + + public static $classMap = array ( + 'Grav\\Plugin\\ChildlistPlugin' => __DIR__ . '/../..' . '/childlist.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit910efd51a496a8e9d2c9ce9e3608936d::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit910efd51a496a8e9d2c9ce9e3608936d::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit910efd51a496a8e9d2c9ce9e3608936d::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/plugins/childlist/vendor/composer/installed.json b/plugins/childlist/vendor/composer/installed.json new file mode 100755 index 0000000..fe51488 --- /dev/null +++ b/plugins/childlist/vendor/composer/installed.json @@ -0,0 +1 @@ +[] diff --git a/plugins/cookieconsent/.gitignore b/plugins/cookieconsent/.gitignore new file mode 100755 index 0000000..40df0b2 --- /dev/null +++ b/plugins/cookieconsent/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.idea +vendor +composer.phar +composer.lock +phpunit.xml +tests/_output/* +tests/_output/* +tests/_output/* +tests/_output/* \ No newline at end of file diff --git a/plugins/cookieconsent/.scrutinizer.yml b/plugins/cookieconsent/.scrutinizer.yml new file mode 100755 index 0000000..1809b3d --- /dev/null +++ b/plugins/cookieconsent/.scrutinizer.yml @@ -0,0 +1,8 @@ +build: + tests: + override: + - + command: 'vendor/bin/codecept run --coverage --coverage-xml' + coverage: + file: 'tests/_output/coverage.xml' + format: 'clover' \ No newline at end of file diff --git a/plugins/cookieconsent/.travis.yml b/plugins/cookieconsent/.travis.yml new file mode 100755 index 0000000..160a7cb --- /dev/null +++ b/plugins/cookieconsent/.travis.yml @@ -0,0 +1,19 @@ +language: php + +php: + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm + +matrix: + allow_failures: + - php: hhvm + +before_install: + - composer --prefer-source install + +script: + - vendor/bin/codecept run \ No newline at end of file diff --git a/plugins/cookieconsent/CHANGELOG.md b/plugins/cookieconsent/CHANGELOG.md new file mode 100755 index 0000000..444b423 --- /dev/null +++ b/plugins/cookieconsent/CHANGELOG.md @@ -0,0 +1,35 @@ +# v0.4.0 +## 06/24/2018 + +1. [](#new) + * Added plugin setting to include cookie consent js and css from official cdn or local + * Added sections to structure plugin settings +2. [](#improved) + * Updated cookie consent js and css from version 3.0.3 to 3.0.6. +3. [](#bugfix) + * Fixed typo in helper text for color of button background. + * Fixed typos in README. + +# v0.3.0 +## 06/24/2018 + +1. [](#new) + * Added color picker to fields with color in blueprint +2. [](#improved) + * Changed the validation for policy link to be less strict to allow absolute as well as relative URl and path +3. [](#bugfix) + * Fixed typo in labels and helper + + +# v0.2.0 +## 10/29/2017 + +1. [](#new) + * Add translations for english, french, spanish, german and italian. + * Translations will be used if no `content_message`, `content_dismiss`, `content_link` or `content_href` are defined in `cookieconsent.yaml`. + +# v0.1.0 +## 08/17/2017 + +1. [](#new) + * ChangeLog started... diff --git a/plugins/cookieconsent/LICENSE b/plugins/cookieconsent/LICENSE new file mode 100755 index 0000000..660d04f --- /dev/null +++ b/plugins/cookieconsent/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 naucon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cookieconsent/README.md b/plugins/cookieconsent/README.md new file mode 100755 index 0000000..b788844 --- /dev/null +++ b/plugins/cookieconsent/README.md @@ -0,0 +1,111 @@ +# Cookie Consent Plugin + +![Cookie Consent](assets/readme_1.png) + +The **Cookie Consent** Plugin is for [Grav CMS](http://github.com/getgrav/grav). This grav plugin is to alert users about the use of cookies on your website. The plugin integrates the popular js lib [cookie consent](https://github.com/insites/cookieconsent/) by insites. + +## Installation + +Installing the Cookieconsent plugin can be done in one of two ways. The GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file. + +### GPM Installation (Preferred) + +The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's terminal (also called the command line). From the root of your Grav install type: + + bin/gpm install cookieconsent + +This will install the Cookieconsent plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/cookieconsent`. + +### Manual Installation + +To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `cookieconsent`. You can find these files on [GitHub](https://github.com/naucon/grav-plugin-cookieconsent) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/cookieconsent + +> NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Error](https://github.com/getgrav/grav-plugin-error) and [Problems](https://github.com/getgrav/grav-plugin-problems) to operate. + +## Configuration + +Before configuring this plugin, you should copy the `user/plugins/cookieconsent/cookieconsent.yaml` to `user/config/plugins/cookieconsent.yaml` and only edit that copy. + +Here is the default configuration: + +```yaml +enabled: true +``` + +## Advanced configuration + +You can customize texts, shape and color with the following available options: + +```yaml +# Include JavaScript and CSS files from official CDN or local +cdn: true +# Message on the banner (overwrite translation) +content_message: 'This website uses cookies to ensure you get the best experience on our website.' +# Dismiss button text (overwrite translation) +content_dismiss: 'Got it!' +# Policy link text (overwrite translation) +content_link: 'Learn more' +# Link to policy (overwrite translation) +content_href: 'https://cookiesandyou.com' + +# Color of banner background +popup_background_color: '#000' +# Color of banner text +popup_text_color: '#fff' +# Color of button background +button_background_color: '#f1d600' +# Color of button text +button_text_color: '#000' +# Color of button border +button_border_color: '#f1d600' + +# Position on the website, where the banner will be displayed. +# top = Top +# top-pushdown: Top (Pushdown) +# bottom: Bottom +# bottom-left: Bottom Left +# bottom-right: Bottom Right +position: bottom + +# Button layout +# block: Block (angled corners) +# classic: Classic (round corners) +# edgeless: Edgeless +theme: block +``` + +### Translation + +Translations are defined in the `languages.yaml` file. + +Translations will be used if no `content_message`, `content_dismiss`, `content_link` or `content_href` are defined in `cookieconsent.yaml`. + + +### Known Issues + +#### Incompatible themes + +The plugin hooks into the grav asset manager (https://learn.getgrav.org/themes/asset-manager). Some themes haven't (properbly) integrated the assets manager and therefore do not work. + +Have a look at the following required elements in the base theme. Without them the plugin will not work. + + {{ assets.css() }} + {{ assets.js() }} + + + +## License + +The MIT License (MIT) + +Copyright (c) 2015 Sven Sanzenbacher + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/plugins/cookieconsent/assets/cookieconsent.min.css b/plugins/cookieconsent/assets/cookieconsent.min.css new file mode 100755 index 0000000..4374f8c --- /dev/null +++ b/plugins/cookieconsent/assets/cookieconsent.min.css @@ -0,0 +1,6 @@ +.cc-window{opacity:1;transition:opacity 1s ease}.cc-window.cc-invisible{opacity:0}.cc-animate.cc-revoke{transition:transform 1s ease}.cc-animate.cc-revoke.cc-top{transform:translateY(-2em)}.cc-animate.cc-revoke.cc-bottom{transform:translateY(2em)}.cc-animate.cc-revoke.cc-active.cc-bottom,.cc-animate.cc-revoke.cc-active.cc-top,.cc-revoke:hover{transform:translateY(0)}.cc-grower{max-height:0;overflow:hidden;transition:max-height 1s} +.cc-link,.cc-revoke:hover{text-decoration:underline}.cc-revoke,.cc-window{position:fixed;overflow:hidden;box-sizing:border-box;font-family:Helvetica,Calibri,Arial,sans-serif;font-size:16px;line-height:1.5em;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;z-index:9999}.cc-window.cc-static{position:static}.cc-window.cc-floating{padding:2em;max-width:24em;-ms-flex-direction:column;flex-direction:column}.cc-window.cc-banner{padding:1em 1.8em;width:100%;-ms-flex-direction:row;flex-direction:row}.cc-revoke{padding:.5em}.cc-header{font-size:18px;font-weight:700}.cc-btn,.cc-close,.cc-link,.cc-revoke{cursor:pointer}.cc-link{opacity:.8;display:inline-block;padding:.2em}.cc-link:hover{opacity:1}.cc-link:active,.cc-link:visited{color:initial}.cc-btn{display:block;padding:.4em .8em;font-size:.9em;font-weight:700;border-width:2px;border-style:solid;text-align:center;white-space:nowrap}.cc-banner .cc-btn:last-child{min-width:140px}.cc-highlight .cc-btn:first-child{background-color:transparent;border-color:transparent}.cc-highlight .cc-btn:first-child:focus,.cc-highlight .cc-btn:first-child:hover{background-color:transparent;text-decoration:underline}.cc-close{display:block;position:absolute;top:.5em;right:.5em;font-size:1.6em;opacity:.9;line-height:.75}.cc-close:focus,.cc-close:hover{opacity:1} +.cc-revoke.cc-top{top:0;left:3em;border-bottom-left-radius:.5em;border-bottom-right-radius:.5em}.cc-revoke.cc-bottom{bottom:0;left:3em;border-top-left-radius:.5em;border-top-right-radius:.5em}.cc-revoke.cc-left{left:3em;right:unset}.cc-revoke.cc-right{right:3em;left:unset}.cc-top{top:1em}.cc-left{left:1em}.cc-right{right:1em}.cc-bottom{bottom:1em}.cc-floating>.cc-link{margin-bottom:1em}.cc-floating .cc-message{display:block;margin-bottom:1em}.cc-window.cc-floating .cc-compliance{-ms-flex:1 0 auto;flex:1 0 auto}.cc-window.cc-banner{-ms-flex-align:center;align-items:center}.cc-banner.cc-top{left:0;right:0;top:0}.cc-banner.cc-bottom{left:0;right:0;bottom:0}.cc-banner .cc-message{-ms-flex:1;flex:1}.cc-compliance{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:justify;align-content:space-between}.cc-compliance>.cc-btn{-ms-flex:1;flex:1}.cc-btn+.cc-btn{margin-left:.5em} +@media print{.cc-revoke,.cc-window{display:none}}@media screen and (max-width:900px){.cc-btn{white-space:normal}}@media screen and (max-width:414px) and (orientation:portrait),screen and (max-width:736px) and (orientation:landscape){.cc-window.cc-top{top:0}.cc-window.cc-bottom{bottom:0}.cc-window.cc-banner,.cc-window.cc-left,.cc-window.cc-right{left:0;right:0}.cc-window.cc-banner{-ms-flex-direction:column;flex-direction:column}.cc-window.cc-banner .cc-compliance{-ms-flex:1;flex:1}.cc-window.cc-floating{max-width:none}.cc-window .cc-message{margin-bottom:1em}.cc-window.cc-banner{-ms-flex-align:unset;align-items:unset}} +.cc-floating.cc-theme-classic{padding:1.2em;border-radius:5px}.cc-floating.cc-type-info.cc-theme-classic .cc-compliance{text-align:center;display:inline;-ms-flex:none;flex:none}.cc-theme-classic .cc-btn{border-radius:5px}.cc-theme-classic .cc-btn:last-child{min-width:140px}.cc-floating.cc-type-info.cc-theme-classic .cc-btn{display:inline-block} +.cc-theme-edgeless.cc-window{padding:0}.cc-floating.cc-theme-edgeless .cc-message{margin:2em 2em 1.5em}.cc-banner.cc-theme-edgeless .cc-btn{margin:0;padding:.8em 1.8em;height:100%}.cc-banner.cc-theme-edgeless .cc-message{margin-left:1em}.cc-floating.cc-theme-edgeless .cc-btn+.cc-btn{margin-left:0} \ No newline at end of file diff --git a/plugins/cookieconsent/assets/cookieconsent.min.js b/plugins/cookieconsent/assets/cookieconsent.min.js new file mode 100755 index 0000000..3c47973 --- /dev/null +++ b/plugins/cookieconsent/assets/cookieconsent.min.js @@ -0,0 +1 @@ +!function(e){if(!e.hasInitialised){var t={escapeRegExp:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},hasClass:function(e,t){var i=" ";return 1===e.nodeType&&(i+e.className+i).replace(/[\n\t]/g,i).indexOf(i+t+i)>=0},addClass:function(e,t){e.className+=" "+t},removeClass:function(e,t){var i=new RegExp("\\b"+this.escapeRegExp(t)+"\\b");e.className=e.className.replace(i,"")},interpolateString:function(e,t){var i=/{{([a-z][a-z0-9\-_]*)}}/gi;return e.replace(i,function(e){return t(arguments[1])||""})},getCookie:function(e){var t="; "+document.cookie,i=t.split("; "+e+"=");return 2!=i.length?void 0:i.pop().split(";").shift()},setCookie:function(e,t,i,n,o){var s=new Date;s.setDate(s.getDate()+(i||365));var r=[e+"="+t,"expires="+s.toUTCString(),"path="+(o||"/")];n&&r.push("domain="+n),document.cookie=r.join(";")},deepExtend:function(e,t){for(var i in t)t.hasOwnProperty(i)&&(i in e&&this.isPlainObject(e[i])&&this.isPlainObject(t[i])?this.deepExtend(e[i],t[i]):e[i]=t[i]);return e},throttle:function(e,t){var i=!1;return function(){i||(e.apply(this,arguments),i=!0,setTimeout(function(){i=!1},t))}},hash:function(e){var t,i,n,o=0;if(0===e.length)return o;for(t=0,n=e.length;t=128?"#000":"#fff"},getLuminance:function(e){var t=parseInt(this.normaliseHex(e),16),i=38,n=(t>>16)+i,o=(t>>8&255)+i,s=(255&t)+i,r=(16777216+65536*(n<255?n<1?0:n:255)+256*(o<255?o<1?0:o:255)+(s<255?s<1?0:s:255)).toString(16).slice(1);return"#"+r},isMobile:function(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)},isPlainObject:function(e){return"object"==typeof e&&null!==e&&e.constructor==Object}};e.status={deny:"deny",allow:"allow",dismiss:"dismiss"},e.transitionEnd=function(){var e=document.createElement("div"),t={t:"transitionend",OT:"oTransitionEnd",msT:"MSTransitionEnd",MozT:"transitionend",WebkitT:"webkitTransitionEnd"};for(var i in t)if(t.hasOwnProperty(i)&&"undefined"!=typeof e.style[i+"ransition"])return t[i];return""}(),e.hasTransition=!!e.transitionEnd;var i=Object.keys(e.status).map(t.escapeRegExp);e.customStyles={},e.Popup=function(){function n(){this.initialise.apply(this,arguments)}function o(e){this.openingTimeout=null,t.removeClass(e,"cc-invisible")}function s(t){t.style.display="none",t.removeEventListener(e.transitionEnd,this.afterTransition),this.afterTransition=null}function r(){var t=this.options.onInitialise.bind(this);if(!window.navigator.cookieEnabled)return t(e.status.deny),!0;if(window.CookiesOK||window.navigator.CookiesOK)return t(e.status.allow),!0;var i=Object.keys(e.status),n=this.getStatus(),o=i.indexOf(n)>=0;return o&&t(n),o}function a(){var e=this.options.position.split("-"),t=[];return e.forEach(function(e){t.push("cc-"+e)}),t}function c(){var e=this.options,i="top"==e.position||"bottom"==e.position?"banner":"floating";t.isMobile()&&(i="floating");var n=["cc-"+i,"cc-type-"+e.type,"cc-theme-"+e.theme];e["static"]&&n.push("cc-static"),n.push.apply(n,a.call(this));p.call(this,this.options.palette);return this.customStyleSelector&&n.push(this.customStyleSelector),n}function l(){var e={},i=this.options;i.showLink||(i.elements.link="",i.elements.messagelink=i.elements.message),Object.keys(i.elements).forEach(function(n){e[n]=t.interpolateString(i.elements[n],function(e){var t=i.content[e];return e&&"string"==typeof t&&t.length?t:""})});var n=i.compliance[i.type];n||(n=i.compliance.info),e.compliance=t.interpolateString(n,function(t){return e[t]});var o=i.layouts[i.layout];return o||(o=i.layouts.basic),t.interpolateString(o,function(t){return e[t]})}function u(i){var n=this.options,o=document.createElement("div"),s=n.container&&1===n.container.nodeType?n.container:document.body;o.innerHTML=i;var r=o.children[0];return r.style.display="none",t.hasClass(r,"cc-window")&&e.hasTransition&&t.addClass(r,"cc-invisible"),this.onButtonClick=h.bind(this),r.addEventListener("click",this.onButtonClick),n.autoAttach&&(s.firstChild?s.insertBefore(r,s.firstChild):s.appendChild(r)),r}function h(n){var o=n.target;if(t.hasClass(o,"cc-btn")){var s=o.className.match(new RegExp("\\bcc-("+i.join("|")+")\\b")),r=s&&s[1]||!1;r&&(this.setStatus(r),this.close(!0))}t.hasClass(o,"cc-close")&&(this.setStatus(e.status.dismiss),this.close(!0)),t.hasClass(o,"cc-revoke")&&this.revokeChoice()}function p(e){var i=t.hash(JSON.stringify(e)),n="cc-color-override-"+i,o=t.isPlainObject(e);return this.customStyleSelector=o?n:null,o&&d(i,e,"."+n),o}function d(i,n,o){if(e.customStyles[i])return void++e.customStyles[i].references;var s={},r=n.popup,a=n.button,c=n.highlight;r&&(r.text=r.text?r.text:t.getContrast(r.background),r.link=r.link?r.link:r.text,s[o+".cc-window"]=["color: "+r.text,"background-color: "+r.background],s[o+".cc-revoke"]=["color: "+r.text,"background-color: "+r.background],s[o+" .cc-link,"+o+" .cc-link:active,"+o+" .cc-link:visited"]=["color: "+r.link],a&&(a.text=a.text?a.text:t.getContrast(a.background),a.border=a.border?a.border:"transparent",s[o+" .cc-btn"]=["color: "+a.text,"border-color: "+a.border,"background-color: "+a.background],"transparent"!=a.background&&(s[o+" .cc-btn:hover, "+o+" .cc-btn:focus"]=["background-color: "+v(a.background)]),c?(c.text=c.text?c.text:t.getContrast(c.background),c.border=c.border?c.border:"transparent",s[o+" .cc-highlight .cc-btn:first-child"]=["color: "+c.text,"border-color: "+c.border,"background-color: "+c.background]):s[o+" .cc-highlight .cc-btn:first-child"]=["color: "+r.text]));var l=document.createElement("style");document.head.appendChild(l),e.customStyles[i]={references:1,element:l.sheet};var u=-1;for(var h in s)s.hasOwnProperty(h)&&l.sheet.insertRule(h+"{"+s[h].join(";")+"}",++u)}function v(e){return e=t.normaliseHex(e),"000000"==e?"#222":t.getLuminance(e)}function f(i){if(t.isPlainObject(i)){var n=t.hash(JSON.stringify(i)),o=e.customStyles[n];if(o&&!--o.references){var s=o.element.ownerNode;s&&s.parentNode&&s.parentNode.removeChild(s),e.customStyles[n]=null}}}function m(e,t){for(var i=0,n=e.length;i=0&&(this.dismissTimeout=window.setTimeout(function(){t(e.status.dismiss)},Math.floor(i)));var n=this.options.dismissOnScroll;if("number"==typeof n&&n>=0){var o=function(i){window.pageYOffset>Math.floor(n)&&(t(e.status.dismiss),window.removeEventListener("scroll",o),this.onWindowScroll=null)};this.onWindowScroll=o,window.addEventListener("scroll",o)}}function y(){if("info"!=this.options.type&&(this.options.revokable=!0),t.isMobile()&&(this.options.animateRevokable=!1),this.options.revokable){var e=a.call(this);this.options.animateRevokable&&e.push("cc-animate"),this.customStyleSelector&&e.push(this.customStyleSelector);var i=this.options.revokeBtn.replace("{{classes}}",e.join(" "));this.revokeBtn=u.call(this,i);var n=this.revokeBtn;if(this.options.animateRevokable){var o=t.throttle(function(e){var i=!1,o=20,s=window.innerHeight-20;t.hasClass(n,"cc-top")&&e.clientYs&&(i=!0),i?t.hasClass(n,"cc-active")||t.addClass(n,"cc-active"):t.hasClass(n,"cc-active")&&t.removeClass(n,"cc-active")},200);this.onMouseMove=o,window.addEventListener("mousemove",o)}}}var g={enabled:!0,container:null,cookie:{name:"cookieconsent_status",path:"/",domain:"",expiryDays:365},onPopupOpen:function(){},onPopupClose:function(){},onInitialise:function(e){},onStatusChange:function(e,t){},onRevokeChoice:function(){},content:{header:"Cookies used on the website!",message:"This website uses cookies to ensure you get the best experience on our website.",dismiss:"Got it!",allow:"Allow cookies",deny:"Decline",link:"Learn more",href:"http://cookiesandyou.com",close:"❌"},elements:{header:'{{header}} ',message:'{{message}}',messagelink:'{{message}} {{link}}',dismiss:'{{dismiss}}',allow:'{{allow}}',deny:'{{deny}}',link:'{{link}}',close:'{{close}}'},window:'',revokeBtn:'
Cookie Policy
',compliance:{info:'
{{dismiss}}
',"opt-in":'
{{dismiss}}{{allow}}
',"opt-out":'
{{deny}}{{dismiss}}
'},type:"info",layouts:{basic:"{{messagelink}}{{compliance}}","basic-close":"{{messagelink}}{{compliance}}{{close}}","basic-header":"{{header}}{{message}}{{link}}{{compliance}}"},layout:"basic",position:"bottom",theme:"block","static":!1,palette:null,revokable:!1,animateRevokable:!0,showLink:!0,dismissOnScroll:!1,dismissOnTimeout:!1,autoOpen:!0,autoAttach:!0,whitelistPage:[],blacklistPage:[],overrideHTML:null};return n.prototype.initialise=function(e){this.options&&this.destroy(),t.deepExtend(this.options={},g),t.isPlainObject(e)&&t.deepExtend(this.options,e),r.call(this)&&(this.options.enabled=!1),m(this.options.blacklistPage,location.pathname)&&(this.options.enabled=!1),m(this.options.whitelistPage,location.pathname)&&(this.options.enabled=!0);var i=this.options.window.replace("{{classes}}",c.call(this).join(" ")).replace("{{children}}",l.call(this)),n=this.options.overrideHTML;if("string"==typeof n&&n.length&&(i=n),this.options["static"]){var o=u.call(this,'
'+i+"
");o.style.display="",this.element=o.firstChild,this.element.style.display="none",t.addClass(this.element,"cc-invisible")}else this.element=u.call(this,i);b.call(this),y.call(this),this.options.autoOpen&&this.autoOpen()},n.prototype.destroy=function(){this.onButtonClick&&this.element&&(this.element.removeEventListener("click",this.onButtonClick),this.onButtonClick=null),this.dismissTimeout&&(clearTimeout(this.dismissTimeout),this.dismissTimeout=null),this.onWindowScroll&&(window.removeEventListener("scroll",this.onWindowScroll),this.onWindowScroll=null),this.onMouseMove&&(window.removeEventListener("mousemove",this.onMouseMove),this.onMouseMove=null),this.element&&this.element.parentNode&&this.element.parentNode.removeChild(this.element),this.element=null,this.revokeBtn&&this.revokeBtn.parentNode&&this.revokeBtn.parentNode.removeChild(this.revokeBtn),this.revokeBtn=null,f(this.options.palette),this.options=null},n.prototype.open=function(t){if(this.element)return this.isOpen()||(e.hasTransition?this.fadeIn():this.element.style.display="",this.options.revokable&&this.toggleRevokeButton(),this.options.onPopupOpen.call(this)),this},n.prototype.close=function(t){if(this.element)return this.isOpen()&&(e.hasTransition?this.fadeOut():this.element.style.display="none",t&&this.options.revokable&&this.toggleRevokeButton(!0),this.options.onPopupClose.call(this)),this},n.prototype.fadeIn=function(){var i=this.element;if(e.hasTransition&&i&&(this.afterTransition&&s.call(this,i),t.hasClass(i,"cc-invisible"))){if(i.style.display="",this.options["static"]){var n=this.element.clientHeight;this.element.parentNode.style.maxHeight=n+"px"}var r=20;this.openingTimeout=setTimeout(o.bind(this,i),r)}},n.prototype.fadeOut=function(){var i=this.element;e.hasTransition&&i&&(this.openingTimeout&&(clearTimeout(this.openingTimeout),o.bind(this,i)),t.hasClass(i,"cc-invisible")||(this.options["static"]&&(this.element.parentNode.style.maxHeight=""),this.afterTransition=s.bind(this,i),i.addEventListener(e.transitionEnd,this.afterTransition),t.addClass(i,"cc-invisible")))},n.prototype.isOpen=function(){return this.element&&""==this.element.style.display&&(!e.hasTransition||!t.hasClass(this.element,"cc-invisible"))},n.prototype.toggleRevokeButton=function(e){this.revokeBtn&&(this.revokeBtn.style.display=e?"":"none")},n.prototype.revokeChoice=function(e){this.options.enabled=!0,this.clearStatus(),this.options.onRevokeChoice.call(this),e||this.autoOpen()},n.prototype.hasAnswered=function(t){return Object.keys(e.status).indexOf(this.getStatus())>=0},n.prototype.hasConsented=function(t){var i=this.getStatus();return i==e.status.allow||i==e.status.dismiss},n.prototype.autoOpen=function(e){!this.hasAnswered()&&this.options.enabled&&this.open()},n.prototype.setStatus=function(i){var n=this.options.cookie,o=t.getCookie(n.name),s=Object.keys(e.status).indexOf(o)>=0;Object.keys(e.status).indexOf(i)>=0?(t.setCookie(n.name,i,n.expiryDays,n.domain,n.path),this.options.onStatusChange.call(this,i,s)):this.clearStatus()},n.prototype.getStatus=function(){return t.getCookie(this.options.cookie.name)},n.prototype.clearStatus=function(){var e=this.options.cookie;t.setCookie(e.name,"",-1,e.domain,e.path)},n}(),e.Location=function(){function e(e){t.deepExtend(this.options={},s),t.isPlainObject(e)&&t.deepExtend(this.options,e),this.currentServiceIndex=-1}function i(e,t,i){var n,o=document.createElement("script");o.type="text/"+(e.type||"javascript"),o.src=e.src||e,o.async=!1,o.onreadystatechange=o.onload=function(){var e=o.readyState;clearTimeout(n),t.done||e&&!/loaded|complete/.test(e)||(t.done=!0,t(),o.onreadystatechange=o.onload=null)},document.body.appendChild(o),n=setTimeout(function(){t.done=!0,t(),o.onreadystatechange=o.onload=null},i)}function n(e,t,i,n,o){var s=new(window.XMLHttpRequest||window.ActiveXObject)("MSXML2.XMLHTTP.3.0");if(s.open(n?"POST":"GET",e,1),s.setRequestHeader("X-Requested-With","XMLHttpRequest"),s.setRequestHeader("Content-type","application/x-www-form-urlencoded"),Array.isArray(o))for(var r=0,a=o.length;r3&&t(s)}),s.send(n)}function o(e){return new Error("Error ["+(e.code||"UNKNOWN")+"]: "+e.error)}var s={timeout:5e3,services:["freegeoip","ipinfo","maxmind"],serviceDefinitions:{freegeoip:function(){return{url:"//freegeoip.net/json/?callback={callback}",isScript:!0,callback:function(e,t){try{var i=JSON.parse(t);return i.error?o(i):{code:i.country_code}}catch(n){return o({error:"Invalid response ("+n+")"})}}}},ipinfo:function(){return{url:"//ipinfo.io",headers:["Accept: application/json"],callback:function(e,t){try{var i=JSON.parse(t);return i.error?o(i):{code:i.country}}catch(n){return o({error:"Invalid response ("+n+")"})}}}},ipinfodb:function(e){return{url:"//api.ipinfodb.com/v3/ip-country/?key={api_key}&format=json&callback={callback}",isScript:!0,callback:function(e,t){try{var i=JSON.parse(t);return"ERROR"==i.statusCode?o({error:i.statusMessage}):{code:i.countryCode}}catch(n){return o({error:"Invalid response ("+n+")"})}}}},maxmind:function(){return{url:"//js.maxmind.com/js/apis/geoip2/v2.1/geoip2.js",isScript:!0,callback:function(e){return window.geoip2?void geoip2.country(function(t){try{e({code:t.country.iso_code})}catch(i){e(o(i))}},function(t){e(o(t))}):void e(new Error("Unexpected response format. The downloaded script should have exported `geoip2` to the global scope"))}}}}};return e.prototype.getNextService=function(){var e;do e=this.getServiceByIdx(++this.currentServiceIndex);while(this.currentServiceIndex=0,revokable:t.revokable.indexOf(e)>=0,explicitAction:t.explicitAction.indexOf(e)>=0}},e.prototype.applyLaw=function(e,t){var i=this.get(t);return i.hasLaw||(e.enabled=!1),this.options.regionalLaw&&(i.revokable&&(e.revokable=!0),i.explicitAction&&(e.dismissOnScroll=!1,e.dismissOnTimeout=!1)),e},e}(),e.initialise=function(t,i,n){var o=new e.Law(t.law);i||(i=function(){}),n||(n=function(){}),e.getCountryCode(t,function(n){delete t.law,delete t.location,n.code&&(t=o.applyLaw(t,n.code)),i(new e.Popup(t))},function(i){delete t.law,delete t.location,n(i,new e.Popup(t))})},e.getCountryCode=function(t,i,n){if(t.law&&t.law.countryCode)return void i({code:t.law.countryCode});if(t.location){var o=new e.Location(t.location);return void o.locate(function(e){i(e||{})},n)}i({})},e.utils=t,e.hasInitialised=!0,window.cookieconsent=e}}(window.cookieconsent||{}); \ No newline at end of file diff --git a/plugins/cookieconsent/assets/readme_1.png b/plugins/cookieconsent/assets/readme_1.png new file mode 100755 index 0000000..116af3c Binary files /dev/null and b/plugins/cookieconsent/assets/readme_1.png differ diff --git a/plugins/cookieconsent/blueprints.yaml b/plugins/cookieconsent/blueprints.yaml new file mode 100755 index 0000000..234fbea --- /dev/null +++ b/plugins/cookieconsent/blueprints.yaml @@ -0,0 +1,152 @@ +name: Cookie Consent +version: 0.4.0 +description: This grav plugin is to alert users about the use of cookies on your website. The plugin integrates the popular js lib cookie consent by insites. +icon: gavel +author: + name: naucon +homepage: https://github.com/naucon/grav-plugin-cookieconsent +keywords: grav, plugin, cookie, consent, eu, law, policy, insites +bugs: https://github.com/naucon/grav-plugin-cookieconsent/issues +docs: https://github.com/naucon/grav-plugin-cookieconsent/blob/develop/README.md +license: MIT + +form: + validation: strict + fields: + basics: + type: section + title: Basics + underline: true + + enabled: + type: toggle + label: Plugin status + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + + cdn: + type: toggle + label: CDN + help: "Include JavaScript and CSS files from official CDN or local." + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + + content: + type: section + title: Content + underline: true + + content_message: + type: text + label: Message + help: Message on the banner + placeholder: "This website uses cookies to ensure you get the best experience on our website." + validate: + type: text + + content_dismiss: + type: text + label: Dismiss button text + placeholder: "Got it!" + validate: + type: text + + content_link: + type: text + label: Policy link text + placeholder: "Learn more" + validate: + type: text + + content_href: + type: text + label: Link to policy + placeholder: "https://cookiesandyou.com" + validate: + type: text + + + colors: + type: section + title: Colors + underline: true + + popup_background_color: + type: colorpicker + label: Banner background + help: Color of banner background + default: #000000 + validate: + type: text + + popup_text_color: + type: colorpicker + label: Banner Text + help: Color of banner text + default: #ffffff + validate: + type: text + + button_background_color: + type: colorpicker + label: Button background + help: Color of button background + default: #f1d600 + validate: + type: text + + button_text_color: + type: colorpicker + label: Button text + help: Color of button text + default: #000000 + validate: + type: text + + button_border_color: + type: colorpicker + label: Button border + help: Color of button border + default: #f1d600 + validate: + type: text + + + style: + type: section + title: Position & Style + underline: true + + position: + type: select + size: medium + label: Position + default: bottom + help: Position on the website, where the banner will be displayed. + options: + top: Top + top-pushdown: Top (Pushdown) + bottom: Bottom + bottom-left: Bottom Left + bottom-right: Bottom Right + + theme: + type: select + size: medium + label: Layout + default: block + help: Button layout + options: + block: Block (angled corners) + classic: Classic (round corners) + edgeless: Edgeless \ No newline at end of file diff --git a/plugins/cookieconsent/codeception.yml b/plugins/cookieconsent/codeception.yml new file mode 100755 index 0000000..035017e --- /dev/null +++ b/plugins/cookieconsent/codeception.yml @@ -0,0 +1,20 @@ +actor: Tester +paths: + tests: tests + log: tests/_output + data: tests/_data + support: tests/_support + envs: tests/_envs +settings: + bootstrap: _bootstrap.php + colors: true + memory_limit: 1024M +extensions: + enabled: + - Codeception\Extension\RunFailed +coverage: + enabled: true + exclude: + - vendor/* +modules: + config: diff --git a/plugins/cookieconsent/composer.json b/plugins/cookieconsent/composer.json new file mode 100755 index 0000000..44a7ef6 --- /dev/null +++ b/plugins/cookieconsent/composer.json @@ -0,0 +1,15 @@ +{ + "name": "naucon/grav-plugin-cookieconsent", + "description": "This grav plugin is to alert users about the use of cookies on your website. The plugin integrates the popular js lib cookie consent by insites.", + "license": "MIT", + "authors": [ + { + "name": "naucon" + } + ], + "require-dev": { + "getgrav/grav": "^1.2", + "codeception/codeception": "~2.2", + "phpunit/php-code-coverage": "~2.0" + } +} \ No newline at end of file diff --git a/plugins/cookieconsent/cookieconsent.php b/plugins/cookieconsent/cookieconsent.php new file mode 100755 index 0000000..07b5f06 --- /dev/null +++ b/plugins/cookieconsent/cookieconsent.php @@ -0,0 +1,72 @@ + ['onPluginsInitialized', 0] + ]; + } + + /** + * Initialize the plugin + */ + public function onPluginsInitialized() + { + // Don't proceed if we are in the admin plugin + if ($this->isAdmin()) { + return; + } + + // Enable the main event we are interested in + $this->enable([ + 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], + 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], + ]); + } + + /** + * Add current directory to twig lookup paths. + */ + public function onTwigTemplatePaths() + { + $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; + } + + /** + * if enabled on this page, load the JS + CSS theme. + */ + public function onTwigSiteVariables() + { + $twig = $this->grav['twig']; + $config = $this->config->toArray(); + + if ($this->config->get('plugins.cookieconsent.cdn')) { + $this->grav['assets']->addCss("//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.0.6/cookieconsent.min.css"); + $this->grav['assets']->addJs("//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.0.6/cookieconsent.min.js"); + } else { + $this->grav['assets']->addCss("plugin://cookieconsent/assets/cookieconsent.min.css"); + $this->grav['assets']->addJs("plugin://cookieconsent/assets/cookieconsent.min.js"); + } + + $this->grav['assets']->addInlineJs($twig->twig->render('partials/cookieconsent.html.twig', array('config' => $config))); + } +} diff --git a/plugins/cookieconsent/cookieconsent.yaml b/plugins/cookieconsent/cookieconsent.yaml new file mode 100755 index 0000000..e559624 --- /dev/null +++ b/plugins/cookieconsent/cookieconsent.yaml @@ -0,0 +1,9 @@ +enabled: true +cdn: true +popup_background_color: '#000000' +popup_text_color: '#ffffff' +button_background_color: '#f1d600' +button_text_color: '#000000' +button_border_color: '#f1d600' +position: bottom +theme: block \ No newline at end of file diff --git a/plugins/cookieconsent/languages.yaml b/plugins/cookieconsent/languages.yaml new file mode 100755 index 0000000..01ee924 --- /dev/null +++ b/plugins/cookieconsent/languages.yaml @@ -0,0 +1,39 @@ +en: + PLUGINS: + COOKIECONSENT: + CONTENT_MESSAGE: "This website uses cookies to ensure you get the best experience on our website." + CONTENT_DISMISS: "Got it!" + CONTENT_LINK: "Learn more" + CONTENT_HREF: "https://cookiesandyou.com" + +fr: + PLUGINS: + COOKIECONSENT: + CONTENT_MESSAGE: "Nous utilisons des cookies pour assurer une meilleure navigation sur notre site. En utilisant nos services, vous acceptez notre utilisation des cookies." + CONTENT_DISMISS: "OK" + CONTENT_LINK: "En savoir plus" + CONTENT_HREF: "https://cookiesandyou.com" + +de: + PLUGINS: + COOKIECONSENT: + CONTENT_MESSAGE: "Diese Webseite verwendet Cookies, um Ihnen einen besseren Service bieten zu können. Durch die weitere Nutzung der Seite stimmen Sie der Verwendung zu." + CONTENT_DISMISS: "OK" + CONTENT_LINK: "Weitere Informationen" + CONTENT_HREF: "https://cookiesandyou.com" + +it: + PLUGINS: + COOKIECONSENT: + CONTENT_MESSAGE: "Utilizziamo i cookie per migliorare i nostri servizi. Avvalendoti dei nostri servizi accetti la politica sull'uso dei cookie." + CONTENT_DISMISS: "OK" + CONTENT_LINK: "scopri di più" + CONTENT_HREF: "https://cookiesandyou.com" + +es: + PLUGINS: + COOKIECONSENT: + CONTENT_MESSAGE: "Usamos cookies para ofrecer nuestros servicios de la mejor forma posible. Al usar nuestros servicios, usted acepta nuestro uso de cookies." + CONTENT_DISMISS: "OK" + CONTENT_LINK: "ver más" + CONTENT_HREF: "https://cookiesandyou.com" \ No newline at end of file diff --git a/plugins/cookieconsent/templates/partials/cookieconsent.html.twig b/plugins/cookieconsent/templates/partials/cookieconsent.html.twig new file mode 100755 index 0000000..f0dcb68 --- /dev/null +++ b/plugins/cookieconsent/templates/partials/cookieconsent.html.twig @@ -0,0 +1,27 @@ +window.addEventListener("load", function(){ +window.cookieconsent.initialise({ +"palette": { + "popup": { + "background": "{{ config.plugins.cookieconsent.popup_background_color }}", + "text": "{{ config.plugins.cookieconsent.popup_text_color }}" + }, + "button": { + "background": "{{ config.plugins.cookieconsent.button_background_color }}", + "text": "{{ config.plugins.cookieconsent.button_text_color }}", + "border": "{{ config.plugins.cookieconsent.button_border_color }}" + } +}, +{% if config.plugins.cookieconsent.position == 'top-pushdown' %} +"position": "top", +"static": true, +{% else %} +"position": "{{ config.plugins.cookieconsent.position }}", +{% endif %} +"theme": "{{ config.plugins.cookieconsent.theme }}", +"content": { + "message": "{% if config.plugins.cookieconsent.content_message %}{{ config.plugins.cookieconsent.content_message }}{% else %}{{ 'PLUGINS.COOKIECONSENT.CONTENT_MESSAGE'|t }}{% endif %}", + "dismiss": "{% if config.plugins.cookieconsent.content_dismiss %}{{ config.plugins.cookieconsent.content_dismiss }}{% else %}{{ 'PLUGINS.COOKIECONSENT.CONTENT_DISMISS'|t }}{% endif %}", + "link": "{% if config.plugins.cookieconsent.content_link %}{{ config.plugins.cookieconsent.content_link }}{% else %}{{ 'PLUGINS.COOKIECONSENT.CONTENT_LINK'|t }}{% endif %}", + "href": "{% if config.plugins.cookieconsent.content_href %}{{ config.plugins.cookieconsent.content_href }}{% else %}{{ 'PLUGINS.COOKIECONSENT.CONTENT_HREF'|t }}{% endif %}" +} +})}); \ No newline at end of file diff --git a/plugins/cookieconsent/tests/_bootstrap.php b/plugins/cookieconsent/tests/_bootstrap.php new file mode 100755 index 0000000..243f9c8 --- /dev/null +++ b/plugins/cookieconsent/tests/_bootstrap.php @@ -0,0 +1,2 @@ +grav = Stub::make(Grav::class); + $this->config = Stub::make(Config::class); + $this->eventDispatcher = Stub::makeEmpty(EventDispatcherInterface::class); + $this->twigEngine = Stub::makeEmpty(\Twig\Environment::class); + $this->twig = Stub::make(Twig::class); + $this->assets = Stub::make(Assets::class); + } + + + public function testGetSubscribedEvents() + { + $expectedResult = [ + 'onPluginsInitialized' => ['onPluginsInitialized', 0] + ]; + + $actualResult = CookieconsentPlugin::getSubscribedEvents(); + + $this->assertEquals($expectedResult, $actualResult); + } + + public function testOnPluginsInitialized() + { + $this->grav['events'] = $this->eventDispatcher; + + $plugin = new CookieconsentPlugin('Cookie Consent', $this->grav, $this->config); + $plugin->onPluginsInitialized(); + } + + public function testOnTwigTemplatePaths() + { + $this->grav['twig'] = $this->twig; + + $plugin = new CookieconsentPlugin('Cookie Consent', $this->grav, $this->config); + $plugin->onTwigTemplatePaths(); + + $this->assertCount(1, $this->twig->twig_paths); + $this->assertContains('/templates', $this->twig->twig_paths[0]); + } + + public function testOnTwigSiteVariables() + { + $this->grav['twig'] = $this->twig; + $this->twig->twig = $this->twigEngine; + + $this->config = Stub::make(Config::class, + [ + 'get' => function($key) { + if ($key == 'plugins.cookieconsent.cdn') { + return true; + } + + return null; + } + ] + ); + + $this->assets = Stub::make(Assets::class, + [ + 'addCss' => function($asset) { + $this->assertEquals('//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.0.6/cookieconsent.min.css', $asset); + }, + 'addJs' => function($asset) { + $this->assertEquals('//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.0.6/cookieconsent.min.js', $asset); + } + ] + ); + $this->grav['assets'] = $this->assets; + + $plugin = new CookieconsentPlugin('Cookie Consent', $this->grav, $this->config); + $plugin->onTwigSiteVariables(); + } + + public function testOnTwigSiteVariablesWithDisabledCdn() + { + $this->grav['twig'] = $this->twig; + $this->twig->twig = $this->twigEngine; + + $this->config = Stub::make(Config::class, + [ + 'get' => function($key) { + if ($key == 'plugins.cookieconsent.cdn') { + return false; + } + + return null; + } + ] + ); + $this->assets = Stub::make(Assets::class, + [ + 'addCss' => function($asset) { + $this->assertEquals('plugin://cookieconsent/assets/cookieconsent.min.css', $asset); + }, + 'addJs' => function($asset) { + $this->assertEquals('plugin://cookieconsent/assets/cookieconsent.min.js', $asset); + } + ] + ); + $this->grav['assets'] = $this->assets; + + + $plugin = new CookieconsentPlugin('Cookie Consent', $this->grav, $this->config); + $plugin->onTwigSiteVariables(); + } +} diff --git a/plugins/cookieconsent/tests/unit/_bootstrap.php b/plugins/cookieconsent/tests/unit/_bootstrap.php new file mode 100755 index 0000000..8a88555 --- /dev/null +++ b/plugins/cookieconsent/tests/unit/_bootstrap.php @@ -0,0 +1,2 @@ + bin/plugin devtools new-plugin +Enter Plugin Name: MyPlugin +Enter Plugin Description: My New Custom Plugin +Enter Developer Name: Johnny Rotten +Enter GitHub ID (can be blank): pretty-vacant +Enter Developer Email: johnny@rotten.com + +SUCCESS plugin myplugin -> Created Successfully + +Path: /home/johnnyr/webroot/grav-installation/user/plugins/myplugin +``` + +## Theme Scaffolding + +To create a new theme you simply need to run: `bin/plugin devtools new-theme` and fill in the few questions at the prompts: + +``` +> bin/plugin devtools new-theme +Enter Theme Name: MyTheme +Enter Theme Description: My New Custom Theme +Enter Developer Name: Johnny Rotten +Enter GitHub ID (can be blank): pretty-vacant +Enter Developer Email: johnny@rotten.com +Please choose a template type + [pure-blank ] Basic Theme using Pure.css + [inheritance] Inherit from another theme + [copy ] Copy another theme + > pure-blank + +SUCCESS theme mytheme -> Created Successfully + +Path: /home/johnnyr/webroot/grav-installation/user/themes/mytheme +``` + +There are **three template creation options** + +1. `pure-blank` - This is a very basic blank theme that uses the [Pure CSS framework](http://purecss.io/) +2. `inheritance` - This creates a very basic template with minimal files that inherits a base theme. To find out more about theme inheritance, [check out the subject in more details on the Grav Learn site](https://learn.getgrav.org/themes/customization#theme-inheritance). +3. `copy` - This allows you to create a new theme based on an existing theme. This is the simplest way to get started with a new theme by using another theme as the basis. + +## Skipping Online Project Name Collision Checking + +By default, devtools will check your project's name with the existing gpm ecosystem to ensure no collisions. In order to skip this check, add an `--offline` or `-o` to your command: + + `bin/plugin devtools new-theme --offline` +or + + `bin/plugin devtools new-theme -o` diff --git a/plugins/devtools/blueprints.yaml b/plugins/devtools/blueprints.yaml new file mode 100644 index 0000000..98eda91 --- /dev/null +++ b/plugins/devtools/blueprints.yaml @@ -0,0 +1,41 @@ +name: DevTools +slug: devtools +type: plugin +version: 1.7.1 +description: Plugin and Theme scaffolding utilities +icon: cogs +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +homepage: https://github.com/getgrav/grav-plugin-devtools +keywords: devtools, plugin, theme +bugs: https://github.com/getgrav/grav-plugin-devtools/issues +license: MIT + +dependencies: + - { name: grav, version: '>=1.7.0' } + +form: + validation: strict + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + collision_check: + type: toggle + label: PLUGIN_DEVTOOLS.COLLISION_CHECK + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/devtools/classes/DevToolsCommand.php b/plugins/devtools/classes/DevToolsCommand.php new file mode 100644 index 0000000..ea036f9 --- /dev/null +++ b/plugins/devtools/classes/DevToolsCommand.php @@ -0,0 +1,380 @@ +init(); + $grav['uri']->init(); + + $this->inflector = $grav['inflector']; + $this->locator = $grav['locator']; + $this->twig = $grav['twig']; + $this->gpm = new GPM(); + + //Add `theme://` to prevent fail + $this->locator->addPath('theme', '', []); + $this->locator->addPath('plugin', '', []); + $this->locator->addPath('blueprint', '', []); + // $this->config->set('theme', $config->get('themes.' . $name)); + } + + /** + * Backwards compatibility to Grav 1.6. + * + * @return InputInterface + */ + public function getInput(): InputInterface + { + return $this->input; + } + + /** + * Backwards compatibility to Grav 1.6. + * + * @return SymfonyStyle + */ + public function getIO(): SymfonyStyle + { + $output = $this->output; + if (!$output instanceof SymfonyStyle) { + $this->output = $output = new SymfonyStyle($this->input, $this->output); + } + + return $this->output; + } + + /** + * Copies the component type and renames accordingly + * + * @return bool + */ + protected function createComponent(): bool + { + $name = $this->component['name']; + $folder_name = strtolower($this->inflector::hyphenize($name)); + $new_theme = $folder_name; + $type = $this->component['type']; + $grav = Grav::instance(); + $config = $grav['config']; + $current_theme = $config->get('system.pages.theme'); + $template = $this->component['template']; + $source_theme = null; + + if (isset($this->component['copy'])) { + $current_theme = $this->component['copy']; + $source_theme = $this->locator->findResource('themes://' . $current_theme); + $template_folder = $source_theme; + } else { + $template_folder = __DIR__ . "/../components/{$type}/{$template}"; + } + + if ($type === 'blueprint') { + $component_folder = $this->locator->findResource('themes://' . $current_theme) . '/blueprints'; + } else { + $component_folder = $this->locator->findResource($type . 's://') . DS . $folder_name; + } + + if (false === $template_folder) { + $this->output->writeln("Theme {$current_theme} does not exist"); + return false; + } + + if ($template === 'inheritance') { + $parent_theme = $this->component['extends']; + $yaml_file = $this->locator->findResource('themes://' . $parent_theme) . '/' . $parent_theme . '.yaml'; + $this->component['config'] = file_get_contents($yaml_file); + } + + if (isset($source_theme)) { + /** + * Copy existing theme and regex-replace old stuff with new + */ + + // Get source if a symlink + if (is_link($template_folder)) { + $template_folder = readlink($template_folder); + if (false === $template_folder) { + $this->output->writeln("Theme {$current_theme} is a bad symlink"); + return false; + } + } + + //Copy All files to component folder + try { + Folder::copy($template_folder, $component_folder, '/.git|node_modules/'); + } catch (\Exception $e) { + $this->output->writeln("" . $e->getMessage() . ""); + return false; + } + + // Do some filename renaming + $base_old_filename = $component_folder . '/' . $current_theme; + $base_new_filename = $component_folder . '/' . $new_theme; + @rename($base_old_filename . '.php', $base_new_filename . '.php'); + @rename($base_old_filename . '.yaml', $base_new_filename . '.yaml'); + + $camelized_current = $this->inflector::camelize($current_theme); + $camelized_new = $this->inflector::camelize($name); + + $hyphenized_current = $this->inflector::hyphenize($current_theme); + $hyphenized_new = $this->inflector::hyphenize($name); + + $titleized_current = $this->inflector::titleize($current_theme); + $titleized_new = $this->inflector::titleize($name); + + $underscoreized_current = $this->inflector::underscorize($current_theme); + $underscoreized_new = $this->inflector::underscorize($name); + + $variations_regex = [ + ["/$camelized_current/", "/$hyphenized_current/"], + [$camelized_new, $hyphenized_new] + ]; + + if (!in_array("/$titleized_current/", array_values($variations_regex[0]))) { + $current_regex = $variations_regex[0]; + $new_regex = $variations_regex[1]; + $current_regex[] = "/$titleized_current/"; + $new_regex[] = $titleized_new; + $variations_regex = [$current_regex, $new_regex]; + } + + if (!in_array("/$underscoreized_current/", array_values($variations_regex[0]))) { + $current_regex = $variations_regex[0]; + $new_regex = $variations_regex[1]; + $current_regex[] = "/$underscoreized_current/"; + $new_regex[] = $underscoreized_new; + $variations_regex = [$current_regex, $new_regex]; + } + + $regex_array = [ + $new_theme . '.php' => $variations_regex, + 'blueprints.yaml' => $variations_regex, + 'README.md' => $variations_regex, + ]; + + foreach ($regex_array as $filename => $data) { + $filename = $component_folder . '/' . $filename; + if (!file_exists($filename)) { + continue; + } + $file = file_get_contents($filename); + if ($file) { + $file = preg_replace($data[0], $data[1], $file); + } + file_put_contents($filename, $file); + } + + echo $source_theme; + } else { + /** + * Use components folder and twig processing + */ + //Copy All files to component folder + try { + Folder::copy($template_folder, $component_folder); + } catch (\Exception $e) { + $this->output->writeln("" . $e->getMessage() . ""); + return false; + } + + //Add Twig vars and templates then initialize + $this->twig->twig_vars['component'] = $this->component; + $this->twig->twig_paths[] = $template_folder; + $this->twig->init(); + + //Get all templates of component then process each with twig and save + $templates = Folder::all($component_folder); + + try { + foreach ($templates as $templateFile) { + if (Utils::endsWith($templateFile, '.twig') && !Utils::endsWith($templateFile, '.html.twig')) { + $content = $this->twig->processTemplate($templateFile); + $file = File::instance($component_folder . DS . str_replace('.twig', '', $templateFile)); + $file->content($content); + $file->save(); + + //Delete twig template + $file = File::instance($component_folder . DS . $templateFile); + $file->delete(); + } + } + } catch (\Exception $e) { + $this->output->writeln("" . $e->getMessage() . ""); + $this->output->writeln("Rolling back..."); + Folder::delete($component_folder); + $this->output->writeln($type . "creation failed!"); + return false; + } + if ($type !== 'blueprint') { + rename($component_folder . DS . $type . '.php', $component_folder . DS . $folder_name . '.php'); + rename($component_folder . DS . $type . '.yaml', $component_folder . DS . $folder_name . '.yaml'); + } else { + $bpname = $this->inflector::hyphenize($this->component['bpname']); + rename($component_folder . DS . $type . '.yaml', $component_folder . DS . $bpname . '.yaml'); + } + + if ($this->component['flex_name']) { + $flex_classes_folder = $component_folder . DS . 'classes' . DS . 'Flex' . DS . 'Types'; + $flex_name = strtolower($this->inflector::underscorize($this->component['flex_name'])); + $flex_name_camel = $this->inflector::camelize($this->component['flex_name']); + + rename($flex_classes_folder . DS . 'flex_name',$flex_classes_folder . DS . $flex_name_camel); + + rename($flex_classes_folder . DS . $flex_name_camel . DS . 'Object' . '.php',$flex_classes_folder . DS . $flex_name_camel . DS . $flex_name_camel . 'Object' . '.php'); + rename($flex_classes_folder . DS . $flex_name_camel . DS . 'Collection' . '.php',$flex_classes_folder . DS . $flex_name_camel . DS . $flex_name_camel . 'Collection' . '.php'); + + rename($component_folder . DS . 'blueprints' . DS . 'flex-objects' . DS . $type . '.yaml', $component_folder . DS . 'blueprints' . DS . 'flex-objects' . DS . $flex_name . '.yaml'); + } + + } + + $this->output->writeln(''); + $this->output->writeln('SUCCESS ' . $type . ' ' . $name . ' -> Created Successfully'); + $this->output->writeln(''); + $this->output->writeln('Path: ' . $component_folder . ''); + $this->output->writeln(''); + if ($type === 'plugin') { + $this->output->writeln('Please run `cd ' . $component_folder . '` and `composer update` to initialize the autoloader'); + $this->output->writeln(''); + } + + return true; + } + + /** + * Iterate through all options and validate + * + * @return void + */ + protected function validateOptions(): void + { + foreach (array_filter($this->options) as $type => $value) { + $this->validate($type, $value); + } + } + + /** + * @param string $type + * @param mixed $value + * @return mixed + */ + protected function validate(string $type, $value) + { + switch ($type) { + case 'name': + // Check if name is empty + if ($value === null || trim($value) === '') { + throw new \RuntimeException('Name cannot be empty'); + } + + // Check if name starts with a numeric character + if (is_numeric($value[0])) { + throw new \RuntimeException('Name must start with an alphabetic character (A-Z, a-z)'); + } + + if (!$this->options['offline']) { + // Check for name collision with online gpm. + if (false !== $this->gpm->findPackage($value)) { + throw new \RuntimeException('Package name exists in GPM'); + } + } else { + $this->output->writeln(''); + $this->output->writeln(' Warning: Please note that by skipping the online check, your project\'s plugin or theme name may conflict with an existing plugin or theme.'); + } + + // Check if it's reserved + if ($this->isReservedWord(strtolower($value))) { + throw new \RuntimeException("\"" . $value . "\" is a reserved word and cannot be used as the name"); + } + + break; + + case 'description': + if ($value === null || trim($value) === '') { + throw new \RuntimeException('Description cannot be empty'); + } + + break; + case 'themename': + if ($value === null || trim($value) === '') { + throw new \RuntimeException('Theme Name cannot be empty'); + } + + break; + case 'developer': + if ($value === null || trim($value) === '') { + throw new \RuntimeException('Developer\'s Name cannot be empty'); + } + + break; + + case 'githubid': + // GitHubID can be blank, so nothing here + break; + + case 'email': + if (!preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $value)) { + throw new \RuntimeException('Not a valid email address'); + } + + break; + } + + return $value; + } + + /** + * @param string $word + * @return bool + */ + public function isReservedWord(string $word): bool + { + return in_array($word, $this->reserved_keywords, true); + } +} diff --git a/plugins/devtools/cli/NewBlueprintCommand.php b/plugins/devtools/cli/NewBlueprintCommand.php new file mode 100644 index 0000000..f1dee47 --- /dev/null +++ b/plugins/devtools/cli/NewBlueprintCommand.php @@ -0,0 +1,88 @@ +setName('new-blueprint') + ->setAliases(['newblueprint','blueprint']) + ->addOption( + 'bpname', + null, + InputOption::VALUE_OPTIONAL, + 'The name of your new Grav theme' + ) + ->addOption( + 'name', + null, + InputOption::VALUE_OPTIONAL, + 'The name of your new Grav theme' + ) + ->addOption( + 'template', + null, + InputOption::VALUE_OPTIONAL, + 'The name/username of the developer' + ) + ->setDescription('Create a blueprint that extend the default.yaml blueprint files') + ->setHelp('The new-blueprint command creates a new blueprint file.'); + } + + /** + * @return int + */ + protected function serve(): int + { + $this->init(); + + $input = $this->getInput(); + $io = $this->getIO(); + + $this->component['type'] = 'blueprint'; + $this->component['template'] = 'modular'; + $this->component['version'] = '0.1.0'; + $this->component['themename'] = 'bonjour'; + + $this->options = [ + 'name' => $input->getOption('name'), + 'bpname' => $input->getOption('bpname'), + 'template' => $input->getOption('template'), + + ]; + + $this->validateOptions(); + + $this->component = array_replace($this->component, $this->options); + + if (!$this->options['template']) { + $question = new ChoiceQuestion('Please choose a template type', ['newtab', 'append']); + + $this->component['template'] = $io->askQuestion($question); + } + if (!$this->options['bpname']) { + $question = new Question('Enter Blueprint Name'); + + $this->component['bpname'] = $io->askQuestion($question); + } + + $this->createComponent(); + + return 0; + } +} diff --git a/plugins/devtools/cli/NewPluginCommand.php b/plugins/devtools/cli/NewPluginCommand.php new file mode 100644 index 0000000..d66941d --- /dev/null +++ b/plugins/devtools/cli/NewPluginCommand.php @@ -0,0 +1,168 @@ +setName('new-plugin') + ->setAliases(['newplugin']) + ->addOption( + 'name', + null, + InputOption::VALUE_OPTIONAL, + 'The name of your new Grav plugin' + ) + ->addOption( + 'desc', + null, + InputOption::VALUE_OPTIONAL, + 'A description of your new Grav plugin' + ) + ->addOption( + 'dev', + null, + InputOption::VALUE_OPTIONAL, + 'The name/username of the developer' + ) + ->addOption( + 'github', + null, + InputOption::VALUE_OPTIONAL, + 'The developer\'s GitHub ID' + ) + ->addOption( + 'email', + 'e', + InputOption::VALUE_OPTIONAL, + 'The developer\'s email' + ) + ->addOption( + 'offline', + 'o', + InputOption::VALUE_NONE, + 'Skip online name collision check' + ) + ->setDescription('Creates a new Grav plugin with the basic required files') + ->setHelp('The new-plugin command creates a new Grav instance and performs the creation of a plugin.'); + } + + /** + * @return int + */ + protected function serve(): int + { + $this->init(); + + $input = $this->getInput(); + $io = $this->getIO(); + + $this->component['type'] = 'plugin'; + $this->component['template'] = 'blank'; + $this->component['version'] = '0.1.0'; + + $this->options = [ + 'name' => $input->getOption('name'), + 'description' => $input->getOption('desc'), + 'author' => [ + 'name' => $input->getOption('dev'), + 'email' => $input->getOption('email'), + 'githubid' => $input->getOption('github') + ], + 'offline' => $input->getOption('offline'), + ]; + + $this->validateOptions(); + + $this->component = array_replace($this->component, $this->options); + + if (!$this->options['name']) { + $question = new Question('Enter Plugin Name'); + $question->setValidator(function ($value) { + return $this->validate('name', $value); + }); + + $this->component['name'] = $io->askQuestion($question); + } + + if (!$this->options['description']) { + $question = new Question('Enter Plugin Description'); + $question->setValidator(function ($value) { + return $this->validate('description', $value); + }); + + $this->component['description'] = $io->askQuestion($question); + } + + if (!$this->options['author']['name']) { + $question = new Question('Enter Developer Name'); + $question->setValidator(function ($value) { + return $this->validate('developer', $value); + }); + + $this->component['author']['name'] = $io->askQuestion($question); + } + + + if (!$this->options['author']['githubid']) { + $question = new Question('Enter GitHub ID (can be blank)'); + $question->setValidator(function ($value) { + return $this->validate('githubid', $value); + }); + + $this->component['author']['githubid'] = $io->askQuestion($question); + } + + if (!$this->options['author']['email']) { + $question = new Question('Enter Developer Email'); + $question->setValidator(function ($value) { + return $this->validate('email', $value); + }); + + $this->component['author']['email'] = $io->askQuestion($question); + } + + $question = new ChoiceQuestion( + 'Please choose an option', + ['blank' => 'Basic Plugin', + 'flex' => 'Basic Plugin prepared for custom Flex Objects' + ] + ); + $this->component['template'] = $io->askQuestion($question); + + if ($this->component['template'] === 'flex') { + + $question = new Question('Enter Flex Object Name'); + $question->setValidator(function ($value) { + return $this->validate('name', $value); + }); + $this->component['flex_name'] = $io->askQuestion($question); + + $question = new ChoiceQuestion('Please choose a storage type', [ + 'simple' => 'Basic Storage (1 file for all objects) - no media support', + 'file' => 'File Storage (1 file per object)', + 'folder' => 'Folder Storage (1 folder per object)' + ]); + $this->component['flex_storage'] = $io->askQuestion($question); + } + + $this->createComponent(); + + return 0; + } +} diff --git a/plugins/devtools/cli/NewThemeCommand.php b/plugins/devtools/cli/NewThemeCommand.php new file mode 100644 index 0000000..089b110 --- /dev/null +++ b/plugins/devtools/cli/NewThemeCommand.php @@ -0,0 +1,177 @@ +setName('new-theme') + ->setAliases(['newtheme']) + ->addOption( + 'name', + null, + InputOption::VALUE_OPTIONAL, + 'The name of your new Grav theme' + ) + ->addOption( + 'desc', + null, + InputOption::VALUE_OPTIONAL, + 'A description of your new Grav theme' + ) + ->addOption( + 'dev', + null, + InputOption::VALUE_OPTIONAL, + 'The name/username of the developer' + ) + ->addOption( + 'github', + null, + InputOption::VALUE_OPTIONAL, + 'The developer\'s GitHub ID' + ) + ->addOption( + 'email', + null, + InputOption::VALUE_OPTIONAL, + 'The developer\'s email' + ) + ->addOption( + 'offline', + 'o', + InputOption::VALUE_NONE, + 'Skip online name collision check' + ) + ->setDescription('Creates a new Grav theme with the basic required files') + ->setHelp('The new-theme command creates a new Grav instance and performs the creation of a theme.'); + } + + /** + * @return int + */ + protected function serve(): int + { + $this->init(); + + $input = $this->getInput(); + $io = $this->getIO(); + + $this->component['type'] = 'theme'; + $this->component['template'] = 'blank'; + $this->component['version'] = '0.1.0'; + + $this->options = [ + 'name' => $input->getOption('name'), + 'description' => $input->getOption('desc'), + 'author' => [ + 'name' => $input->getOption('dev'), + 'email' => $input->getOption('email'), + 'githubid' => $input->getOption('github'), + ], + 'offline' => $input->getOption('offline'), + ]; + + $this->validateOptions(); + + $this->component = array_replace($this->component, $this->options); + + if (!$this->options['name']) { + $question = new Question('Enter Theme Name'); + $question->setValidator(function ($value) { + return $this->validate('name', $value); + }); + + $this->component['name'] = $io->askQuestion($question); + } + + if (!$this->options['description']) { + $question = new Question('Enter Theme Description'); + $question->setValidator(function ($value) { + return $this->validate('description', $value); + }); + + $this->component['description'] = $io->askQuestion($question); + } + + if (!$this->options['author']['name']) { + $question = new Question('Enter Developer Name'); + $question->setValidator(function ($value) { + return $this->validate('developer', $value); + }); + + $this->component['author']['name'] = $io->askQuestion($question); + } + + if (!$this->options['author']['githubid']) { + $question = new Question('Enter GitHub ID (can be blank)'); + $question->setValidator(function ($value) { + return $this->validate('githubid', $value); + }); + + $this->component['author']['githubid'] = $io->askQuestion($question); + } + + if (!$this->options['author']['email']) { + $question = new Question('Enter Developer Email'); + $question->setValidator(function ($value) { + return $this->validate('email', $value); + }); + + $this->component['author']['email'] = $io->askQuestion($question); + } + + $question = new ChoiceQuestion( + 'Please choose an option', + [ + 'pure-blank' => 'Basic Theme using Pure.css', + 'tailwind' => 'Basic Theme using tailwind.css and including Alpine.js', + 'tailwind-mix' => 'Same as `tailwind` but more opinionated with Laravel Mix compiler', + 'inheritance' => 'Inherit from another theme', + 'copy' => 'Copy another theme' + ] + ); + $this->component['template'] = $io->askQuestion($question); + + if ($this->component['template'] === 'inheritance') { + $themes = $this->gpm->getInstalledThemes(); + $installedThemes = []; + foreach ($themes as $key => $theme) { + $installedThemes[] = $key; + } + + $question = new ChoiceQuestion('Please choose a theme to extend', $installedThemes); + $this->component['extends'] = $io->askQuestion($question); + } elseif ($this->component['template'] === 'copy') { + $themes = $this->gpm->getInstalledThemes(); + $installedThemes = []; + foreach ($themes as $key => $theme) { + $installedThemes[] = $key; + } + + $question = new ChoiceQuestion( + 'Please choose a theme to copy', + $installedThemes + ); + $this->component['copy'] = $io->askQuestion($question); + } + $this->createComponent(); + + return 0; + } +} diff --git a/plugins/devtools/components/blueprint/append/blueprint.yaml.twig b/plugins/devtools/components/blueprint/append/blueprint.yaml.twig new file mode 100644 index 0000000..8db427f --- /dev/null +++ b/plugins/devtools/components/blueprint/append/blueprint.yaml.twig @@ -0,0 +1,21 @@ +title: {{ component.bpname }} +extends@: + type: default + context: blueprints://pages + +form: + fields: + tabs: + type: tabs + active: 1 + + fields: + content: + fields: + header.an_example_text_field: + type: text + label: Add a number + default: 5 + validate: + required: true + type: int \ No newline at end of file diff --git a/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig b/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig new file mode 100644 index 0000000..d110556 --- /dev/null +++ b/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig @@ -0,0 +1,15 @@ +title: Item +extends@: + type: default + context: blueprints://pages + +form: + fields: + tabs: + fields: + blog: + type: tab + title: {{ component.bpname }} + + fields: + header.mytextfield: \ No newline at end of file diff --git a/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig b/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig new file mode 100644 index 0000000..973fb27 --- /dev/null +++ b/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig @@ -0,0 +1,5 @@ +# v0.1.0 +## {{ "now"|date("m/d/Y") }} + +1. [](#new) + * ChangeLog started... diff --git a/plugins/devtools/components/plugin/blank/LICENSE.twig b/plugins/devtools/components/plugin/blank/LICENSE.twig new file mode 100644 index 0000000..6f88097 --- /dev/null +++ b/plugins/devtools/components/plugin/blank/LICENSE.twig @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) {{ "now"|date("Y") }} {{ component.author.name }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/devtools/components/plugin/blank/README.md.twig b/plugins/devtools/components/plugin/blank/README.md.twig new file mode 100644 index 0000000..a64e3fd --- /dev/null +++ b/plugins/devtools/components/plugin/blank/README.md.twig @@ -0,0 +1,59 @@ +{% set component_title = (component.name|titleize) %} +{% set component_hyphenated = (component.name|hyphenize) %} +{% set developer_hyphenated = (component.author.githubid|hyphenize) %} +# {{ component_title }} Plugin + +**This README.md file should be modified to describe the features, installation, configuration, and general usage of the plugin.** + +The **{{ component_title }}** Plugin is an extension for [Grav CMS](https://github.com/getgrav/grav). {{ component.description }} + +## Installation + +Installing the {{ component_title }} plugin can be done in one of three ways: The GPM (Grav Package Manager) installation method lets you quickly install the plugin with a simple terminal command, the manual method lets you do so via a zip file, and the admin method lets you do so via the Admin Plugin. + +### GPM Installation (Preferred) + +To install the plugin via the [GPM](https://learn.getgrav.org/cli-console/grav-cli-gpm), through your system's terminal (also called the command line), navigate to the root of your Grav-installation, and enter: + + bin/gpm install {{ component_hyphenated }} + +This will install the {{ component_title }} plugin into your `/user/plugins`-directory within Grav. Its files can be found under `/your/site/grav/user/plugins/{{ component_hyphenated }}`. + +### Manual Installation + +To install the plugin manually, download the zip-version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `{{ component_hyphenated }}`. You can find these files on [GitHub](https://github.com/{{ developer_hyphenated }}/grav-plugin-{{ component_hyphenated }}) or via [GetGrav.org](https://getgrav.org/downloads/plugins). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/{{ component_hyphenated }} + +> NOTE: This plugin is a modular component for Grav which may require other plugins to operate, please see its [blueprints.yaml-file on GitHub](https://github.com/{{ developer_hyphenated }}/grav-plugin-{{ component_hyphenated }}/blob/main/blueprints.yaml). + +### Admin Plugin + +If you use the Admin Plugin, you can install the plugin directly by browsing the `Plugins`-menu and clicking on the `Add` button. + +## Configuration + +Before configuring this plugin, you should copy the `user/plugins/{{ component_hyphenated }}/{{ component_hyphenated }}.yaml` to `user/config/plugins/{{ component_hyphenated }}.yaml` and only edit that copy. + +Here is the default configuration and an explanation of available options: + +```yaml +enabled: true +``` + +Note that if you use the Admin Plugin, a file with your configuration named {{component_hyphenated}}.yaml will be saved in the `user/config/plugins/`-folder once the configuration is saved in the Admin. + +## Usage + +**Describe how to use the plugin.** + +## Credits + +**Did you incorporate third-party code? Want to thank somebody?** + +## To Do + +- [ ] Future plans, if any + diff --git a/plugins/devtools/components/plugin/blank/blueprints.yaml.twig b/plugins/devtools/components/plugin/blank/blueprints.yaml.twig new file mode 100644 index 0000000..bd0defd --- /dev/null +++ b/plugins/devtools/components/plugin/blank/blueprints.yaml.twig @@ -0,0 +1,37 @@ +{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%} +name: {{ component.name|titleize }} +slug: {{ component.name|hyphenize }} +type: plugin +version: 0.1.0 +description: {{ component.description }} +icon: plug +author: + name: {{ component.author.name }} + email: {{ component.author.email }} +homepage: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }} +demo: http://demo.yoursite.com +keywords: grav, plugin, etc +bugs: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}/issues +docs: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}/blob/develop/README.md +license: MIT + +dependencies: + - { name: grav, version: '>=1.6.0' } + +form: + validation: loose + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + text_var: + type: text + label: PLUGIN_{{ component.name|underscorize|upper }}.TEXT_VARIABLE + help: PLUGIN_{{ component.name|underscorize|upper }}.TEXT_VARIABLE_HELP diff --git a/plugins/devtools/components/plugin/blank/classes/.gitkeep b/plugins/devtools/components/plugin/blank/classes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/plugin/blank/composer.json.twig b/plugins/devtools/components/plugin/blank/composer.json.twig new file mode 100644 index 0000000..28d8691 --- /dev/null +++ b/plugins/devtools/components/plugin/blank/composer.json.twig @@ -0,0 +1,25 @@ +{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%} +{ + "name": "{{ githubid|lower }}/{{ component.name|hyphenize }}", + "type": "grav-plugin", + "description": "{{ component.description }}", + "keywords": ["plugin"], + "homepage": "https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}", + "license": "MIT", + "authors": [ + { + "name": "{{ component.author.name }}", + "email": "{{ component.author.email }}", + "role": "Developer" + } + ], + "require": { + "php": ">=7.1.3" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\{{ component.name|camelize }}\\": "classes/" + }, + "classmap": ["{{ component.name|hyphenize }}.php"] + } +} diff --git a/plugins/devtools/components/plugin/blank/languages.yaml.twig b/plugins/devtools/components/plugin/blank/languages.yaml.twig new file mode 100644 index 0000000..855cd0e --- /dev/null +++ b/plugins/devtools/components/plugin/blank/languages.yaml.twig @@ -0,0 +1,4 @@ +en: + PLUGIN_{{ component.name|underscorize|upper }}: + TEXT_VARIABLE: Text Variable + TEXT_VARIABLE_HELP: Text to add to the top of a page diff --git a/plugins/devtools/components/plugin/blank/plugin.php.twig b/plugins/devtools/components/plugin/blank/plugin.php.twig new file mode 100644 index 0000000..0daecc4 --- /dev/null +++ b/plugins/devtools/components/plugin/blank/plugin.php.twig @@ -0,0 +1,59 @@ + [ + // Uncomment following line when plugin requires Grav < 1.7 + // ['autoload', 100000], + ['onPluginsInitialized', 0] + ] + ]; + } + + /** + * Composer autoload + * + * @return ClassLoader + */ + public function autoload(): ClassLoader + { + return require __DIR__ . '/vendor/autoload.php'; + } + + /** + * Initialize the plugin + */ + public function onPluginsInitialized(): void + { + // Don't proceed if we are in the admin plugin + if ($this->isAdmin()) { + return; + } + + // Enable the main events we are interested in + $this->enable([ + // Put your main events here + ]); + } +} diff --git a/plugins/devtools/components/plugin/blank/plugin.yaml.twig b/plugins/devtools/components/plugin/blank/plugin.yaml.twig new file mode 100644 index 0000000..55e2f2e --- /dev/null +++ b/plugins/devtools/components/plugin/blank/plugin.yaml.twig @@ -0,0 +1,2 @@ +enabled: true +text_var: Custom Text added by the **{{ component.name|titleize }}** plugin (disable plugin to remove) diff --git a/plugins/devtools/components/plugin/flex/CHANGELOG.md.twig b/plugins/devtools/components/plugin/flex/CHANGELOG.md.twig new file mode 100644 index 0000000..37256e0 --- /dev/null +++ b/plugins/devtools/components/plugin/flex/CHANGELOG.md.twig @@ -0,0 +1,5 @@ +# v0.1.0 +## {{ "now"|date("m/d/Y") }} + +1. [](#new) + * ChangeLog started... diff --git a/plugins/devtools/components/plugin/flex/LICENSE.twig b/plugins/devtools/components/plugin/flex/LICENSE.twig new file mode 100644 index 0000000..015a4ae --- /dev/null +++ b/plugins/devtools/components/plugin/flex/LICENSE.twig @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) {{ "now"|date("Y") }} {{ component.author.name }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/devtools/components/plugin/flex/README.md.twig b/plugins/devtools/components/plugin/flex/README.md.twig new file mode 100644 index 0000000..a64e3fd --- /dev/null +++ b/plugins/devtools/components/plugin/flex/README.md.twig @@ -0,0 +1,59 @@ +{% set component_title = (component.name|titleize) %} +{% set component_hyphenated = (component.name|hyphenize) %} +{% set developer_hyphenated = (component.author.githubid|hyphenize) %} +# {{ component_title }} Plugin + +**This README.md file should be modified to describe the features, installation, configuration, and general usage of the plugin.** + +The **{{ component_title }}** Plugin is an extension for [Grav CMS](https://github.com/getgrav/grav). {{ component.description }} + +## Installation + +Installing the {{ component_title }} plugin can be done in one of three ways: The GPM (Grav Package Manager) installation method lets you quickly install the plugin with a simple terminal command, the manual method lets you do so via a zip file, and the admin method lets you do so via the Admin Plugin. + +### GPM Installation (Preferred) + +To install the plugin via the [GPM](https://learn.getgrav.org/cli-console/grav-cli-gpm), through your system's terminal (also called the command line), navigate to the root of your Grav-installation, and enter: + + bin/gpm install {{ component_hyphenated }} + +This will install the {{ component_title }} plugin into your `/user/plugins`-directory within Grav. Its files can be found under `/your/site/grav/user/plugins/{{ component_hyphenated }}`. + +### Manual Installation + +To install the plugin manually, download the zip-version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `{{ component_hyphenated }}`. You can find these files on [GitHub](https://github.com/{{ developer_hyphenated }}/grav-plugin-{{ component_hyphenated }}) or via [GetGrav.org](https://getgrav.org/downloads/plugins). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/{{ component_hyphenated }} + +> NOTE: This plugin is a modular component for Grav which may require other plugins to operate, please see its [blueprints.yaml-file on GitHub](https://github.com/{{ developer_hyphenated }}/grav-plugin-{{ component_hyphenated }}/blob/main/blueprints.yaml). + +### Admin Plugin + +If you use the Admin Plugin, you can install the plugin directly by browsing the `Plugins`-menu and clicking on the `Add` button. + +## Configuration + +Before configuring this plugin, you should copy the `user/plugins/{{ component_hyphenated }}/{{ component_hyphenated }}.yaml` to `user/config/plugins/{{ component_hyphenated }}.yaml` and only edit that copy. + +Here is the default configuration and an explanation of available options: + +```yaml +enabled: true +``` + +Note that if you use the Admin Plugin, a file with your configuration named {{component_hyphenated}}.yaml will be saved in the `user/config/plugins/`-folder once the configuration is saved in the Admin. + +## Usage + +**Describe how to use the plugin.** + +## Credits + +**Did you incorporate third-party code? Want to thank somebody?** + +## To Do + +- [ ] Future plans, if any + diff --git a/plugins/devtools/components/plugin/flex/blueprints.yaml.twig b/plugins/devtools/components/plugin/flex/blueprints.yaml.twig new file mode 100644 index 0000000..bd0defd --- /dev/null +++ b/plugins/devtools/components/plugin/flex/blueprints.yaml.twig @@ -0,0 +1,37 @@ +{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%} +name: {{ component.name|titleize }} +slug: {{ component.name|hyphenize }} +type: plugin +version: 0.1.0 +description: {{ component.description }} +icon: plug +author: + name: {{ component.author.name }} + email: {{ component.author.email }} +homepage: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }} +demo: http://demo.yoursite.com +keywords: grav, plugin, etc +bugs: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}/issues +docs: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}/blob/develop/README.md +license: MIT + +dependencies: + - { name: grav, version: '>=1.6.0' } + +form: + validation: loose + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + text_var: + type: text + label: PLUGIN_{{ component.name|underscorize|upper }}.TEXT_VARIABLE + help: PLUGIN_{{ component.name|underscorize|upper }}.TEXT_VARIABLE_HELP diff --git a/plugins/devtools/components/plugin/flex/blueprints/flex-objects/plugin.yaml.twig b/plugins/devtools/components/plugin/flex/blueprints/flex-objects/plugin.yaml.twig new file mode 100644 index 0000000..a6556c9 --- /dev/null +++ b/plugins/devtools/components/plugin/flex/blueprints/flex-objects/plugin.yaml.twig @@ -0,0 +1,132 @@ +title: '{{ component.flex_name|capitalize }}' +description: '{{ component.flex_name|capitalize }}' +type: flex-objects + +# Flex Configuration +config: + # Administration Configuration + admin: + # Admin router (optional) + router: + path: '/{{ component.flex_name|lower|underscorize }}' + + # Admin menu (optional) + menu: + list: + route: '/{{ component.flex_name|lower|underscorize }}' + title: '{{ component.flex_name|capitalize }}' + icon: fa-address-card-o + # Authorization to collection admin + authorize: ['admin.{{ component.flex_name|lower|underscorize }}.list', 'admin.super'] + # Priority -10 .. 10 (highest goes up) + priority: 2 + + # Admin template type / folder + template: default + + # Permissions + permissions: + # Primary permissions + admin.{{ component.flex_name|lower|underscorize }}: + type: crudpl + label: '{{ component.flex_name|capitalize }}' + + # List view + list: + title: name + fields: + published: + field: + type: toggle + label: Published + width: 8 + name: + link: edit + description: + + # Edit View + edit: + title: + template: '{{ object.name }}' + + # Preview View + preview: + enabled: false + route: + #template: '/plugins/flex-objects/directory:contacts' + + # Data Export + export: + enabled: true + method: 'jsonSerialize' + formatter: + class: 'Grav\Framework\File\Formatter\YamlFormatter' + filename: '{{ component.flex_name|lower|underscorize }}' + + # Site Configuration + site: + templates: + collection: + # Lookup for the template layout files for collections of objects + paths: + - 'flex/{TYPE}/collection/{LAYOUT}{EXT}' + object: + # Lookup for the template layout files for objects + paths: + - 'flex/{TYPE}/object/{LAYOUT}{EXT}' + defaults: + # Default template {TYPE}; overridden by filename of this blueprint if template folder exists + type: '{{ component.flex_name|lower|underscorize }}' + # Default template {LAYOUT}; can be overridden in render calls (usually Twig in templates) + layout: default + + # Data Configuration + data: + # Object class to be used, allowing custom methods for the object + object: 'Grav\Plugin\{{ component.name|camelize }}\Flex\Types\{{ component.flex_name|camelize }}\{{ component.flex_name|camelize }}Object' + # Collection class to be used, allowing custom methods for the collections + collection: 'Grav\Plugin\{{ component.name|camelize }}\Flex\Types\{{ component.flex_name|camelize }}\{{ component.flex_name|camelize }}Collection' + # Index class to be used, works as a quick database-like lookup index + index: 'Grav\Common\Flex\Types\Generic\GenericIndex' + storage: + # Storage class, use single file storage (does not support images and assets) + class: 'Grav\Framework\Flex\Storage\{{ component.flex_storage|capitalize }}Storage' + options: + formatter: + # File formatter class, in this case the file is stored in markdown + class: 'Grav\Framework\File\Formatter\JsonFormatter' + # JSON file where all the objects will be stored + folder: user-data://{{ component.flex_name|underscorize }} + search: + # Search options + options: + contains: 1 + # Fields to be searched + fields: + - name + - description + +form: + validation: loose + fields: + published: + type: toggle + label: Published + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + required: true + name: + type: text + label: Name + validate: + required: true + description: + type: text + label: Description + validate: + required: true diff --git a/plugins/devtools/components/plugin/flex/classes/.gitkeep b/plugins/devtools/components/plugin/flex/classes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/plugin/flex/classes/Flex/Types/flex_name/Collection.php.twig b/plugins/devtools/components/plugin/flex/classes/Flex/Types/flex_name/Collection.php.twig new file mode 100644 index 0000000..efbc20d --- /dev/null +++ b/plugins/devtools/components/plugin/flex/classes/Flex/Types/flex_name/Collection.php.twig @@ -0,0 +1,25 @@ + + */ +class {{ component.flex_name|camelize }}Collection extends GenericCollection +{ + +} diff --git a/plugins/devtools/components/plugin/flex/classes/Flex/Types/flex_name/Object.php.twig b/plugins/devtools/components/plugin/flex/classes/Flex/Types/flex_name/Object.php.twig new file mode 100644 index 0000000..07cf6a7 --- /dev/null +++ b/plugins/devtools/components/plugin/flex/classes/Flex/Types/flex_name/Object.php.twig @@ -0,0 +1,25 @@ + +*/ +class {{ component.flex_name|camelize }}Object extends GenericObject +{ + +} diff --git a/plugins/devtools/components/plugin/flex/composer.json.twig b/plugins/devtools/components/plugin/flex/composer.json.twig new file mode 100644 index 0000000..28d8691 --- /dev/null +++ b/plugins/devtools/components/plugin/flex/composer.json.twig @@ -0,0 +1,25 @@ +{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%} +{ + "name": "{{ githubid|lower }}/{{ component.name|hyphenize }}", + "type": "grav-plugin", + "description": "{{ component.description }}", + "keywords": ["plugin"], + "homepage": "https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}", + "license": "MIT", + "authors": [ + { + "name": "{{ component.author.name }}", + "email": "{{ component.author.email }}", + "role": "Developer" + } + ], + "require": { + "php": ">=7.1.3" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\{{ component.name|camelize }}\\": "classes/" + }, + "classmap": ["{{ component.name|hyphenize }}.php"] + } +} diff --git a/plugins/devtools/components/plugin/flex/languages.yaml.twig b/plugins/devtools/components/plugin/flex/languages.yaml.twig new file mode 100644 index 0000000..855cd0e --- /dev/null +++ b/plugins/devtools/components/plugin/flex/languages.yaml.twig @@ -0,0 +1,4 @@ +en: + PLUGIN_{{ component.name|underscorize|upper }}: + TEXT_VARIABLE: Text Variable + TEXT_VARIABLE_HELP: Text to add to the top of a page diff --git a/plugins/devtools/components/plugin/flex/plugin.php.twig b/plugins/devtools/components/plugin/flex/plugin.php.twig new file mode 100644 index 0000000..f8f5d8a --- /dev/null +++ b/plugins/devtools/components/plugin/flex/plugin.php.twig @@ -0,0 +1,76 @@ + 0, + ]; + + /** + * @return array + * + * The getSubscribedEvents() gives the core a list of events + * that the plugin wants to listen to. The key of each + * array section is the event that the plugin listens to + * and the value (in the form of an array) contains the + * callable (or function) as well as the priority. The + * higher the number the higher the priority. + */ + public static function getSubscribedEvents(): array + { + return [ + 'onPluginsInitialized' => [ + // Uncomment following line when plugin requires Grav < 1.7 + // ['autoload', 100000], + ['onPluginsInitialized', 0] + ], + FlexRegisterEvent::class => [['onRegisterFlex', 0]], + ]; + } + + /** + * Composer autoload + * + * @return ClassLoader + */ + public function autoload(): ClassLoader + { + return require __DIR__ . '/vendor/autoload.php'; + } + + /** + * Initialize the plugin + */ + public function onPluginsInitialized(): void + { + // Don't proceed if we are in the admin plugin + if ($this->isAdmin()) { + return; + } + + // Enable the main events we are interested in + $this->enable([ + // Put your main events here + ]); + } + + public function onRegisterFlex($event): void + { + $flex = $event->flex; + + $flex->addDirectoryType( + '{{ component.flex_name|lower|underscorize }}', + 'blueprints://flex-objects/{{ component.flex_name|lower|underscorize }}.yaml' + ); + + } +} diff --git a/plugins/devtools/components/plugin/flex/plugin.yaml.twig b/plugins/devtools/components/plugin/flex/plugin.yaml.twig new file mode 100644 index 0000000..55e2f2e --- /dev/null +++ b/plugins/devtools/components/plugin/flex/plugin.yaml.twig @@ -0,0 +1,2 @@ +enabled: true +text_var: Custom Text added by the **{{ component.name|titleize }}** plugin (disable plugin to remove) diff --git a/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig b/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig new file mode 100644 index 0000000..973fb27 --- /dev/null +++ b/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig @@ -0,0 +1,5 @@ +# v0.1.0 +## {{ "now"|date("m/d/Y") }} + +1. [](#new) + * ChangeLog started... diff --git a/plugins/devtools/components/theme/inheritance/LICENSE.twig b/plugins/devtools/components/theme/inheritance/LICENSE.twig new file mode 100644 index 0000000..6f88097 --- /dev/null +++ b/plugins/devtools/components/theme/inheritance/LICENSE.twig @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) {{ "now"|date("Y") }} {{ component.author.name }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/devtools/components/theme/inheritance/README.md.twig b/plugins/devtools/components/theme/inheritance/README.md.twig new file mode 100644 index 0000000..ee5c0b7 --- /dev/null +++ b/plugins/devtools/components/theme/inheritance/README.md.twig @@ -0,0 +1,7 @@ +# {{ component.name|titleize }} Theme + +The **{{ component.name|titleize }}** Theme is for [Grav CMS](http://github.com/getgrav/grav). This README.md file should be modified to describe the features, installation, configuration, and general usage of this theme. + +## Description + +{{ component.description }} diff --git a/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig b/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig new file mode 100644 index 0000000..91700a3 --- /dev/null +++ b/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig @@ -0,0 +1,19 @@ +{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%} +name: {{ component.name|titleize }} +slug: {{ component.name|hyphenize }} +type: theme +version: 0.1.0 +description: {{ component.description }} +icon: rebel +author: + name: {{ component.author.name }} + email: {{ component.author.email }} +homepage: https://github.com/{{ githubid }}/grav-theme-{{ component.name|hyphenize }} +demo: http://demo.yoursite.com +keywords: grav, theme, etc +bugs: https://github.com/{{ githubid }}/grav-theme-{{ component.name|hyphenize }}/issues +readme: https://github.com/{{ githubid }}/grav-theme-{{ component.name|hyphenize }}/blob/develop/README.md +license: MIT + +dependencies: + - { name: grav, version: '>=1.6.0' } diff --git a/plugins/devtools/components/theme/inheritance/css/.gitkeep b/plugins/devtools/components/theme/inheritance/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/inheritance/js/.gitkeep b/plugins/devtools/components/theme/inheritance/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/inheritance/screenshot.jpg b/plugins/devtools/components/theme/inheritance/screenshot.jpg new file mode 100644 index 0000000..5205ca5 Binary files /dev/null and b/plugins/devtools/components/theme/inheritance/screenshot.jpg differ diff --git a/plugins/devtools/components/theme/inheritance/templates/.gitkeep b/plugins/devtools/components/theme/inheritance/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/inheritance/theme.php.twig b/plugins/devtools/components/theme/inheritance/theme.php.twig new file mode 100644 index 0000000..c05016c --- /dev/null +++ b/plugins/devtools/components/theme/inheritance/theme.php.twig @@ -0,0 +1,9 @@ +=1.6.0' } + +form: + validation: loose + fields: + dropdown.enabled: + type: toggle + label: Dropdown in Menu + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/devtools/components/theme/pure-blank/css/custom.css b/plugins/devtools/components/theme/pure-blank/css/custom.css new file mode 100644 index 0000000..e796328 --- /dev/null +++ b/plugins/devtools/components/theme/pure-blank/css/custom.css @@ -0,0 +1,175 @@ +/* Core Stuff */ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + font-size: 1rem; + line-height: 1.7; + color: #606d6e; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #454B4D; +} + +a { + color: #1F8CD6; + text-decoration: none; +} + +a:hover { + color: #175E91; +} + +pre { + background: #F0F0F0; + margin: 1rem 0; + border-radius: 2px; +} + +blockquote { + border-left: 10px solid #eee; + margin: 0; + padding: 0 2rem; +} + +/* Utility Classes */ +.wrapper { + margin: 0 3rem; +} + +.padding { + padding: 3rem 1rem; +} + +.left { + float: left; +} + +.right { + float: right +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-left { + text-align: left; +} + +/* Content Styling */ +.header .padding { + padding: 1rem 0; +} + +.header { + background-color: #1F8DD6; + color: #eee; +} + +.header a { + color: #fff; +} + +.header .logo { + font-size: 1.7rem; + text-transform: uppercase; +} + +.footer { + background-color: #eee; +} + +/* Menu Settings */ +.main-nav ul { + text-align: center; + letter-spacing: -1em; + margin: 0; + padding: 0; +} + +.main-nav ul li { + display: inline-block; + letter-spacing: normal; +} + +.main-nav ul li a { + position: relative; + display: block; + line-height: 45px; + color: #fff; + padding: 0 20px; + white-space: nowrap; +} + +.main-nav > ul > li > a { + border-radius: 2px; +} + +/*Active dropdown nav item */ +.main-nav ul li:hover > a { + background-color: #175E91; +} + +/* Selected Dropdown nav item */ +.main-nav ul li.selected > a { + background-color: #fff; + color: #175E91; +} + +/* Dropdown CSS */ +.main-nav ul li {position: relative;} + +.main-nav ul li ul { + position: absolute; + background-color: #1F8DD6; + min-width: 100%; + text-align: left; + z-index: 999; + + display: none; +} +.main-nav ul li ul li { + display: block; +} + +/* Dropdown CSS */ +.main-nav ul li ul ul { + left: 100%; + top: 0; +} + +/* Active on Hover */ +.main-nav li:hover > ul { + display: block; +} + +/* Child Indicator */ +.main-nav .has-children > a { + padding-right: 30px; +} +.main-nav .has-children > a:after { + font-family: FontAwesome; + content: '\f107'; + position: absolute; + display: inline-block; + right: 8px; + top: 0; +} + +.main-nav .has-children .has-children > a:after { + content: '\f105'; +} diff --git a/plugins/devtools/components/theme/pure-blank/fonts/.gitkeep b/plugins/devtools/components/theme/pure-blank/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/pure-blank/images/logo.png b/plugins/devtools/components/theme/pure-blank/images/logo.png new file mode 100644 index 0000000..64be1a9 Binary files /dev/null and b/plugins/devtools/components/theme/pure-blank/images/logo.png differ diff --git a/plugins/devtools/components/theme/pure-blank/js/.gitkeep b/plugins/devtools/components/theme/pure-blank/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/pure-blank/screenshot.jpg b/plugins/devtools/components/theme/pure-blank/screenshot.jpg new file mode 100644 index 0000000..5205ca5 Binary files /dev/null and b/plugins/devtools/components/theme/pure-blank/screenshot.jpg differ diff --git a/plugins/devtools/components/theme/pure-blank/templates/default.html.twig b/plugins/devtools/components/theme/pure-blank/templates/default.html.twig new file mode 100644 index 0000000..1e97738 --- /dev/null +++ b/plugins/devtools/components/theme/pure-blank/templates/default.html.twig @@ -0,0 +1,5 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} + {{ page.content|raw }} +{% endblock %} diff --git a/plugins/devtools/components/theme/pure-blank/templates/error.html.twig b/plugins/devtools/components/theme/pure-blank/templates/error.html.twig new file mode 100644 index 0000000..c945464 --- /dev/null +++ b/plugins/devtools/components/theme/pure-blank/templates/error.html.twig @@ -0,0 +1,8 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} +
+

Error!

+ {{ page.content|raw }} +
+{% endblock %} diff --git a/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig b/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig new file mode 100644 index 0000000..c4e7e60 --- /dev/null +++ b/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig @@ -0,0 +1,70 @@ + + + +{% block head %} + + {% if header.title %}{{ header.title|e }} | {% endif %}{{ site.title|e }} + + + + {% include 'partials/metadata.html.twig' %} + + + +{% endblock head %} + +{% block stylesheets %} + {% do assets.addCss('https://unpkg.com/purecss@1.0.0/build/pure-min.css', 100) %} + {% do assets.addCss('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', 99) %} + {% do assets.addCss('theme://css/custom.css', 98) %} +{% endblock %} + +{% block javascripts %} + {% do assets.addJs('jquery', 100) %} +{% endblock %} + +{% block assets deferred %} + {{ assets.css()|raw }} + {{ assets.js()|raw }} +{% endblock %} + + + +{% block header %} +
+
+ + {% block header_navigation %} + + {% endblock %} +
+
+{% endblock %} + +{% block body %} +
+
+ {% block content %}{% endblock %} +
+
+{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block bottom %} + {{ assets.js('bottom')|raw }} +{% endblock %} + + + diff --git a/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig b/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig new file mode 100644 index 0000000..28df6aa --- /dev/null +++ b/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig @@ -0,0 +1,47 @@ +{% macro loop(page) %} + {% for p in page.children.visible %} + {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %} + {% if p.children.visible.count > 0 %} +
  • + + {% if p.header.icon %}{% endif %} + {{ p.menu|e }} + +
      + {{ _self.loop(p) }} +
    +
  • + {% else %} +
  • + + {% if p.header.icon %}{% endif %} + {{ p.menu|e }} + +
  • + {% endif %} + {% endfor %} +{% endmacro %} + + diff --git a/plugins/devtools/components/theme/pure-blank/theme.php.twig b/plugins/devtools/components/theme/pure-blank/theme.php.twig new file mode 100644 index 0000000..332b164 --- /dev/null +++ b/plugins/devtools/components/theme/pure-blank/theme.php.twig @@ -0,0 +1,9 @@ +=1.6.0' } + +form: + validation: loose + fields: + dropdown.enabled: + type: toggle + label: Dropdown in Menu + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + production: + type: toggle + label: Production Mode + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/devtools/components/theme/tailwind-mix/css/site.css b/plugins/devtools/components/theme/tailwind-mix/css/site.css new file mode 100644 index 0000000..f578962 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/css/site.css @@ -0,0 +1,5 @@ +/*@import 'yourcssfile.css';*/ + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/plugins/devtools/components/theme/tailwind-mix/fonts/.gitkeep b/plugins/devtools/components/theme/tailwind-mix/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/tailwind-mix/images/logo.png b/plugins/devtools/components/theme/tailwind-mix/images/logo.png new file mode 100644 index 0000000..64be1a9 Binary files /dev/null and b/plugins/devtools/components/theme/tailwind-mix/images/logo.png differ diff --git a/plugins/devtools/components/theme/tailwind-mix/js/.gitkeep b/plugins/devtools/components/theme/tailwind-mix/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/tailwind-mix/js/main.js b/plugins/devtools/components/theme/tailwind-mix/js/main.js new file mode 100644 index 0000000..8647582 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/js/main.js @@ -0,0 +1,5 @@ +import Alpine from 'alpinejs' + +window.Alpine = Alpine + +Alpine.start() \ No newline at end of file diff --git a/plugins/devtools/components/theme/tailwind-mix/languages/en.yaml.twig b/plugins/devtools/components/theme/tailwind-mix/languages/en.yaml.twig new file mode 100644 index 0000000..2b48ba2 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/languages/en.yaml.twig @@ -0,0 +1,2 @@ +THEME_{{ component.name|hyphenize|replace({'-': '_'})|upper }}: + ERROR: 'Error!' diff --git a/plugins/devtools/components/theme/tailwind-mix/package.json.twig b/plugins/devtools/components/theme/tailwind-mix/package.json.twig new file mode 100644 index 0000000..09c8658 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/package.json.twig @@ -0,0 +1,25 @@ +{ + "name": "{{ component.name }}", + "repository": "", + "private": true, + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "npx mix", + "watch": "npx mix watch", + "prod": "npx mix -p" + }, + "dependencies": {}, + "devDependencies": { + "@tailwindcss/forms": "^0.5.0", + "@tailwindcss/typography": "^0.5.2", + "alpinejs": "^2.8.2", + "browser-sync": "^2.27.7", + "browser-sync-webpack-plugin": "^2.3.0", + "laravel-mix": "^6.0.43", + "mix-tailwindcss": "^1.3.0", + "tailwindcss": "^3.0.18", + "tailwindcss-debug-screens": "^2.2.1" + } +} diff --git a/plugins/devtools/components/theme/tailwind-mix/screenshot.jpg b/plugins/devtools/components/theme/tailwind-mix/screenshot.jpg new file mode 100644 index 0000000..5205ca5 Binary files /dev/null and b/plugins/devtools/components/theme/tailwind-mix/screenshot.jpg differ diff --git a/plugins/devtools/components/theme/tailwind-mix/tailwind.config.js.twig b/plugins/devtools/components/theme/tailwind-mix/tailwind.config.js.twig new file mode 100644 index 0000000..9aa99b6 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/tailwind.config.js.twig @@ -0,0 +1,70 @@ +const colors = require('tailwindcss/colors'); + +module.exports = { + content: [ + '../../config/**/*.yaml', + '../../pages/**/*.md', + './blueprints/**/*.yaml', + './js/**/*.js', + './templates/**/*.twig', + './{{ component.name|hyphenize }}.yaml', + './{{ component.name|hyphenize }}.php' + ], + darkMode: 'class', //false or 'media' or 'class' + theme: { + extend: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px' + } + }, + colors: { + 'primary': { + 'lighter': colors.yellow['300'], + DEFAULT: colors.yellow['400'], + 'darker' : colors.yellow['500'], + }, + black: colors.black, + white: colors.white, + red: colors.red, + green: colors.green, + blue: colors.blue, + orange: colors.orange, + indigo: colors.indigo, + transparent: 'transparent', + 'inherit': 'inherit', + }, + typography: (theme) => ({ + DEFAULT: { + css: { + color: 'inherit', + lineHeight: 'inherit', + maxWidth: 'inherit', + a: { + transition: 'all 500ms', + color: theme('colors.primary.DEFAULT'), + '&:hover': { + color: theme('colors.primary.darker') + }, + textDecoration: 'none' + }, + strong: { + color: 'inherit' + }, + } + } + }), + }, + variants: { + extend: {}, + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('tailwindcss-debug-screens') + ], + important: false, +} diff --git a/plugins/devtools/components/theme/tailwind-mix/templates/default.html.twig b/plugins/devtools/components/theme/tailwind-mix/templates/default.html.twig new file mode 100644 index 0000000..1e97738 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/templates/default.html.twig @@ -0,0 +1,5 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} + {{ page.content|raw }} +{% endblock %} diff --git a/plugins/devtools/components/theme/tailwind-mix/templates/error.html.twig b/plugins/devtools/components/theme/tailwind-mix/templates/error.html.twig new file mode 100644 index 0000000..c25efaa --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/templates/error.html.twig @@ -0,0 +1,8 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} +
    +

    {{ 'THEME_TAILWIND.ERROR'|t }}

    + {{ page.content|raw }} +
    +{% endblock %} diff --git a/plugins/devtools/components/theme/tailwind-mix/templates/partials/base.html.twig b/plugins/devtools/components/theme/tailwind-mix/templates/partials/base.html.twig new file mode 100644 index 0000000..4ec88dd --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/templates/partials/base.html.twig @@ -0,0 +1,68 @@ +{% set extension = config.theme.production ? '.min' : '' %} + + + + {% block head %} + + {% if header.title %}{{ header.title|e }} | {% endif %}{{ site.title|e }} + + + + {% include 'partials/metadata.html.twig' %} + + + + {% endblock head %} + + {% block stylesheets %} + {% do assets.addCss('theme://dist/css/site' ~ extension ~ '.css', 98) %} + {% endblock %} + + {% block javascripts %} + {% do assets.addJs('theme://dist/js/main' ~ extension ~ '.js', 98) %} + {% endblock %} + + {% block assets deferred %} + {{ assets.css()|raw }} + {{ assets.js()|raw }} + {% endblock %} + + + +{% block header %} +
    +
    + + {% block header_navigation %} + + {% endblock %} +
    +
    +{% endblock %} + +{% block body %} +
    +
    + {% block content %}{% endblock %} +
    +
    +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block bottom %} + {{ assets.js('bottom')|raw }} +{% endblock %} + + + diff --git a/plugins/devtools/components/theme/tailwind-mix/templates/partials/navigation.html.twig b/plugins/devtools/components/theme/tailwind-mix/templates/partials/navigation.html.twig new file mode 100644 index 0000000..28df6aa --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/templates/partials/navigation.html.twig @@ -0,0 +1,47 @@ +{% macro loop(page) %} + {% for p in page.children.visible %} + {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %} + {% if p.children.visible.count > 0 %} +
  • + + {% if p.header.icon %}{% endif %} + {{ p.menu|e }} + +
      + {{ _self.loop(p) }} +
    +
  • + {% else %} +
  • + + {% if p.header.icon %}{% endif %} + {{ p.menu|e }} + +
  • + {% endif %} + {% endfor %} +{% endmacro %} + + diff --git a/plugins/devtools/components/theme/tailwind-mix/theme.php.twig b/plugins/devtools/components/theme/tailwind-mix/theme.php.twig new file mode 100644 index 0000000..332b164 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind-mix/theme.php.twig @@ -0,0 +1,9 @@ +=1.6.0' } + +form: + validation: loose + fields: + dropdown.enabled: + type: toggle + label: Dropdown in Menu + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + production: + type: toggle + label: Production Mode + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/devtools/components/theme/tailwind/css/site.css b/plugins/devtools/components/theme/tailwind/css/site.css new file mode 100644 index 0000000..f578962 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/css/site.css @@ -0,0 +1,5 @@ +/*@import 'yourcssfile.css';*/ + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/plugins/devtools/components/theme/tailwind/fonts/.gitkeep b/plugins/devtools/components/theme/tailwind/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/tailwind/images/logo.png b/plugins/devtools/components/theme/tailwind/images/logo.png new file mode 100644 index 0000000..64be1a9 Binary files /dev/null and b/plugins/devtools/components/theme/tailwind/images/logo.png differ diff --git a/plugins/devtools/components/theme/tailwind/js/.gitkeep b/plugins/devtools/components/theme/tailwind/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/devtools/components/theme/tailwind/js/main.js b/plugins/devtools/components/theme/tailwind/js/main.js new file mode 100644 index 0000000..8647582 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/js/main.js @@ -0,0 +1,5 @@ +import Alpine from 'alpinejs' + +window.Alpine = Alpine + +Alpine.start() \ No newline at end of file diff --git a/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig b/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig new file mode 100644 index 0000000..2b48ba2 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig @@ -0,0 +1,2 @@ +THEME_{{ component.name|hyphenize|replace({'-': '_'})|upper }}: + ERROR: 'Error!' diff --git a/plugins/devtools/components/theme/tailwind/package-lock.json b/plugins/devtools/components/theme/tailwind/package-lock.json new file mode 100644 index 0000000..f8d4913 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/package-lock.json @@ -0,0 +1,94 @@ +{ + "name": "tailwind", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + } + }, + "dependencies": { + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + } + } +} diff --git a/plugins/devtools/components/theme/tailwind/package.json.twig b/plugins/devtools/components/theme/tailwind/package.json.twig new file mode 100644 index 0000000..7cacd40 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/package.json.twig @@ -0,0 +1,25 @@ +{ + "name": "{{ component.name }}", + "repository": "", + "private": true, + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "cross-env NODE_ENV=development tailwindcss -i ./css/site.css -o ./dist/css/site.css --postcss", + "watch": "cross-env NODE_ENV=development tailwindcss -i ./css/site.css -o ./dist/css/site.css --postcss --watch", + "prod" : "cross-env NODE_ENV=production tailwindcss -i ./css/site.css -o ./dist/css/site.css --postcss" + }, + "dependencies": {}, + "devDependencies": { + "@tailwindcss/forms": "^0.5.3", + "@tailwindcss/typography": "^0.5.9", + "alpinejs": "^2.8.2", + "autoprefixer": "^10.4.13", + "cross-env": "^7.0.3", + "postcss": "^8.4.21", + "postcss-import": "^15.1.0", + "tailwindcss": "^3.2.6", + "tailwindcss-debug-screens": "^2.2.1" + } +} diff --git a/plugins/devtools/components/theme/tailwind/postcss.config.js b/plugins/devtools/components/theme/tailwind/postcss.config.js new file mode 100644 index 0000000..98680a1 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/postcss.config.js @@ -0,0 +1,10 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + ...process.env.NODE_ENV === 'production' + ? {'cssnano': {}} : {} + } +} diff --git a/plugins/devtools/components/theme/tailwind/screenshot.jpg b/plugins/devtools/components/theme/tailwind/screenshot.jpg new file mode 100644 index 0000000..5205ca5 Binary files /dev/null and b/plugins/devtools/components/theme/tailwind/screenshot.jpg differ diff --git a/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig b/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig new file mode 100644 index 0000000..846e3f4 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig @@ -0,0 +1,22 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + '../../config/**/*.yaml', + '../../pages/**/*.md', + './blueprints/**/*.yaml', + './js/**/*.js', + './templates/**/*.twig', + './{{ component.name|hyphenize }}.yaml', + './{{ component.name|hyphenize }}.php' + ], + darkMode: 'class', //false or 'media' or 'class' + theme: {}, + variants: { + extend: {} + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('tailwindcss-debug-screens') + ] +} diff --git a/plugins/devtools/components/theme/tailwind/templates/default.html.twig b/plugins/devtools/components/theme/tailwind/templates/default.html.twig new file mode 100644 index 0000000..1e97738 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/templates/default.html.twig @@ -0,0 +1,5 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} + {{ page.content|raw }} +{% endblock %} diff --git a/plugins/devtools/components/theme/tailwind/templates/error.html.twig b/plugins/devtools/components/theme/tailwind/templates/error.html.twig new file mode 100644 index 0000000..c25efaa --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/templates/error.html.twig @@ -0,0 +1,8 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} +
    +

    {{ 'THEME_TAILWIND.ERROR'|t }}

    + {{ page.content|raw }} +
    +{% endblock %} diff --git a/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig b/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig new file mode 100644 index 0000000..ae825a0 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig @@ -0,0 +1,68 @@ +{% set extension = config.theme.production ? '.min' : '' %} + + + + {% block head %} + + {% if header.title %}{{ header.title|e }} | {% endif %}{{ site.title|e }} + + + + {% include 'partials/metadata.html.twig' %} + + + + {% endblock head %} + + {% block stylesheets %} + {% do assets.addCss('theme://dist/css/site' ~ extension ~ '.css', 98) %} + {% endblock %} + + {% block javascripts %} + {% do assets.addJs('theme://js/main' ~ extension ~ '.js', 98) %} + {% endblock %} + + {% block assets deferred %} + {{ assets.css()|raw }} + {{ assets.js()|raw }} + {% endblock %} + + + +{% block header %} +
    +
    + + {% block header_navigation %} + + {% endblock %} +
    +
    +{% endblock %} + +{% block body %} +
    +
    + {% block content %}{% endblock %} +
    +
    +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block bottom %} + {{ assets.js('bottom')|raw }} +{% endblock %} + + + diff --git a/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig b/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig new file mode 100644 index 0000000..28df6aa --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig @@ -0,0 +1,47 @@ +{% macro loop(page) %} + {% for p in page.children.visible %} + {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %} + {% if p.children.visible.count > 0 %} +
  • + + {% if p.header.icon %}{% endif %} + {{ p.menu|e }} + +
      + {{ _self.loop(p) }} +
    +
  • + {% else %} +
  • + + {% if p.header.icon %}{% endif %} + {{ p.menu|e }} + +
  • + {% endif %} + {% endfor %} +{% endmacro %} + + diff --git a/plugins/devtools/components/theme/tailwind/theme.php.twig b/plugins/devtools/components/theme/tailwind/theme.php.twig new file mode 100644 index 0000000..332b164 --- /dev/null +++ b/plugins/devtools/components/theme/tailwind/theme.php.twig @@ -0,0 +1,9 @@ +`) and simple arrays (`[user@example.com, Joe Bloggs]`) + * Support `tags` and `metadata` in `Email::buildMessage()` + * Two new events `onEmailEngines` and `onEmailTransportDsn` to facilitate the integration of custom engines via plugins + +# v4.0.0-rc.4 +## 02/27/2023 + +1. [](#bugfix) + * Fixed for multiple recipients [#167](https://github.com/getgrav/grav-plugin-email/issues/167) + * Fix for simple array format with names which wasn't working + +# v4.0.0-rc.3 +## 10/27/2022 + +1. [](#bugfix) + * Fixed an issue with array based email address + +# v4.0.0-rc.2 +## 10/19/2022 + +1. [](#improved) + * Set `sendmail` as default engine to provide better fallback for unsupported `mailer` option + * Added info about available engine plugins in `README.md` + +# v4.0.0-rc.1 +## 10/05/2022 + +1. [](#new) + * Migrated from `Swiftmailer` (which has been deprecated) to `Symfony/Mailer`, a more modern and extensible mailing library. + * Built-in support for `SMTP`, `SMTPS`, `Sendmail` and `Native` (uses best solution per-platform) engines + * Added ability to have 3rd party plugins that provide new engines (e.g. `SendGrid`, `MailJet`, etc.) with `SMTP`, `API` or `HTTPS` transports for faster and more reliable email sending + * More flexible options for email formatting including RFC822 `name-addr` format (`Joe Bloggs `) and simple arrays (`[user@example.com, Joe Bloggs]`) + * Support `tags` and `metadata` in `Email::buildMessage()` + * Two new events `onEmailEngines` and `onEmailTransportDsn` to facilitate the integration of custom engines via plugins + +# v3.2.0 +## 03/28/2022 + +1. [](#new) + * Require **Grav 1.7.32** and **Form 6.0.0** +1. [](#improved) + * Added access email message object inside the twig template file +2. [](#bugfix) + * Fixed twig not being initialized when building an email message + +# v3.1.5 +## 01/03/2022 + +1. [](#improved) + * Updated to Swiftmailer `v6.3.0` with PHP 8.1 compatibility [#157](https://github.com/getgrav/grav-plugin-email/issues/157) + +# v3.1.4 +## 11/16/2021 + +1. [](#improved) + * Added second parameter to `Email::send()` to get failed recipients + +# v3.1.3 +## 07/19/2021 + +1. [](#improved) + * Pass page variable to processed forms [#141](https://github.com/getgrav/grav-plugin-email/pull/141) + * Email configuration available to templates [#152](https://github.com/getgrav/grav-plugin-email/pull/152) + * New Event after eMail was sent [#151](https://github.com/getgrav/grav-plugin-email/pull/151) + +# v3.1.2 +## 04/06/2021 + +1. [](#new) + * Added new `onEmailMessage` event to make object available for editing [#150](https://github.com/getgrav/grav-plugin-email/pull/150) + +# v3.1.1 +## 01/31/2021 + +1. [](#improved) + * Latest vendor updates including SwiftMailer `6.2.5` + * Updated CLI commands + * Minor code cleanup + +# v3.1.0 +## 12/02/2020 + +1. [](#improved) + * Added support for `auth_mode` in SMTP engine [#101](https://github.com/getgrav/grav-plugin-email/pull/101) + * Obfuscate the password shown in the CLI `test-email` command [#140](https://github.com/getgrav/grav-plugin-email/pull/140) + +# v3.0.10 +## 11/09/2020 + +1. [](#improved) + * Tweaked default `base.html.twig` template to better support dark-mode clients + * Latest vendor updates +1. [](#bugfix) + * Add missing support for `template:` in body array + * Added check to process markdown with `text/html` content type only + +# v3.0.9 +## 06/08/2020 + +1. [](#improved) + * Disable password autocomplete in password field + * Don't save empty string in password field [#134](https://github.com/getgrav/grav-plugin-email/issues/134) + +# v3.0.8 +## 04/27/2020 + +1. [](#improved) + * Updated vendor library files + * Use Grav's Parsedown class + +# v3.0.7 +## 03/05/2020 + +1. [](#improved) + * Updated email validator library +1. [](#bugfix) + * Fixed `Invalid resource theme://` on CLI command `test-email` on Grav 1.6.21 and later versions [#128](https://github.com/getgrav/grav-plugin-email/issues/128) + +# v3.0.6 +## 02/11/2020 + +1. [](#improved) + * Updated email validator library + +# v3.0.5 +## 02/03/2020 + +1. [](#bugfix) + * Fixed a date in changelog (no other changes) + +# v3.0.4 +## 01/17/2020 + +1. [](#improved) + * Added ZOHO configuration example + * Updated SwiftMailer library for PHP 7.4 support + +# v3.0.3 +## 08/16/2019 + +1. [](#new) + * Support an array of multiple emails in `email:` form process + * Allow form values in email templates +1. [](#improved) + * Added Twig blocks for `content` and `footer` in `email/base.html.twig` template + * Updated `README.md` to reflect working setup for GMail + +# v3.0.2 +## 05/09/2019 + +1. [](#new) + * Requires Form Plugin v3.0.3 + * Added Russian translation [#113](https://github.com/getgrav/grav-plugin-email/pull/113) +1. [](#bugfix) + * Better fix for missing attachments when sending an email using a form [form#333](https://github.com/getgrav/grav-plugin-form/issues/333) + +# v3.0.1 +## 04/15/2019 + +1. [](#improved) + * Put a `try/catch` around email attachments and log any errors rather than hard fail +1. [](#bugfix) + * Fixed missing attachments when sending an email using a form [form#333](https://github.com/getgrav/grav-plugin-form/issues/333) + +# v3.0.0 +## 04/11/2019 + +1. [](#new) + * Added new `template:` to choose twig template option for email form processing + * Moved `buildMessage()` and `parseAddressValue()` to Email object and made public + * Refactored the `EmailUtils::sendEmail()` to take an array of params or the old param list + * Switched to SwiftMailer v.6.1.3 (requires PHP7/Grav 1.6) + * SwiftMailer 6.x compatibility fixes + * Updated various translations + * Added support for Email Queue with Scheduler support + * Code cleanup, composer update + * Added a new `clear-queue-failures` CLI command to flush out failed sends +1. [](#improved) + * Added backlink for scheduler task + * Added support for `environment` option to `flushqueue` CLI command + * Fixed mailtrap hostname in README.md + * Disable autocomplete on SMTP `user` and `password` fields + +# v2.7.2 +## 01/25/2019 + +1. [](#improved) + * Added default for `to` address + * Updated EN language [#99](https://github.com/getgrav/grav-plugin-email/pull/99) + * Updated UK language [#98](https://github.com/getgrav/grav-plugin-email/pull/98) + * Updated RU language [#100](https://github.com/getgrav/grav-plugin-email/pull/100) + * Updated to SwiftMailer v5.4.12 +1. [](#bugfix) + * Fixed `mailtrap` hostname + +# v2.7.1 +## 12/05/2017 + +1. [](#new) + * Added new `onEmailSend()` event hook before sending [#70](https://github.com/getgrav/grav-plugin-email/pull/70) +1. [](#improved) + * Added examples of setting up Email plugin with various SMTP providers + * Updated RU language [#60](https://github.com/getgrav/grav-plugin-email/pull/60) + * Updated to SwiftMailer v5.4.8 + +# v2.7.0 +## 10/26/2017 + +1. [](#improved) + * Now uses a dedicated `logs/email.log` file when `debug: true` + * Improved the README.txt file with examples, and troubleshooting + * Changed default engine to `sendmail` as `mail` is deprecated and not functioning [swiftmailer#866](https://github.com/swiftmailer/swiftmailer/issues/866} + +# v2.6.2 +## 09/30/2017 + +1. [](#improved) + * Removed extraneous files from vendor folder + +# v2.6.1 +## 09/07/2017 + +1. [](#improved) + * Improved the error message when missing `from` in the configuration + * Silently catch malformed email exceptions + +# v2.6.0 +## 05/22/2017 + +1. [](#improved) + * Inherit options from plugin configuration [#39](https://github.com/getgrav/grav-plugin-email/pull/39) +1. [](#bugfix) + * Also process translation on the email subject [https://github.com/getgrav/grav-plugin-comments/issues/38](https://github.com/getgrav/grav-plugin-comments/issues/38) + +# v2.5.3 +## 01/03/2017 + +1. [](#improved) + * Updated to SwiftMailer 5.4.5 [#45](https://github.com/getgrav/grav-plugin-email/issues/45) + +# v2.5.2 +## 12/13/2016 + +1. [](#new) + * RC released as stable + +# v2.5.2-rc.1 +## 11/26/2016 + +1. [](#new) + * Added a new `process_markdown` option for emails in forms +1. [](#improved) + * Improved the `Utils::sendEmail()` method to take the email type as an option + +# v2.5.1 +## 10/19/2016 + +1. [](#improved) + * CLI command will fallback to use the `to` from email plugin config if not provided + * Explicit Composer based class loader to fix issues with class case + +# v2.5.0 +## 09/07/2016 + +1. [](#new) + * Added a new `bin/plugin email test-email` CLI command +1. [](#improved) + * Moved Email `Utils` class from Login to Email plugin + * Provide a sample base `email/base.html.twig` template for emails +1. [](#bugfix) + * Fix handling attachments with the updated file upload field + +# v2.4.3 +## 08/16/2016 + +1. [](#improved) + * Added Russian translation + * Updated Swiftmailer to 5.4.3 [#37](https://github.com/getgrav/grav-plugin-email/issues/37) + +# v2.4.2 +## 08/10/2016 + +1. [](#improved) + * Added Croatian translation + +# v2.4.1 +## 07/14/2016 + +1. [](#improved) + * Allow multiple email recipients (comma separated) [#31](https://github.com/getgrav/grav-plugin-email/issues/31) + * Added Danish and Spanish translations + +# v2.4.0 +## 05/11/2016 + +1. [](#improved) + * Now includes Swiftmailer v5.4.2 which introduces a number of bug fixes and improvements +1. [](#bugfix) + * Correct `starttls` implementation, bundled in TLS + +# v2.3.0 +## 04/20/2016 + +1. [](#improved) + * Added debug option to enable logging on SwiftMailer. + * Updated SwiftMailer from v5.1.0 to v5.4.1. + * Added an option in the Admin settings to enable `starttls` +1. [](#bugfix) + * Correctly name TLS in the Admin settings, the label was `TTS` (but the value was correctly named `tls`) + +# v2.2.0 +## 02/05/2016 + +1. [](#new) + * Allow to send attachments in forms + * Added French translation +1. [](#improved) + * Throw an exception when trying to send emails without a `from` or `to` parameters setup, to intercept less meaningful errors and provide a better description on how to fix the problem + * Changed SMTP password in admin to use a password field instead of plain text + +# v2.1.0 +## 12/18/2015 + +1. [](#new) + * Added missing `content_type` to email.yaml + * Added default values for CC and BCC + 1. [](#improved) + * Improved documentation of new email params in `README.md` + * Moved config setting of `mailer.default` to `mailer.engine` + +# v2.0.0 +## 12/11/2015 + +1. [](#new) + * Added support for from/sender name (Thomas Keitel) + * Added support for message content type (Thomas Keitel) + * Added support for reply addresses (Thomas Keitel) + * Added support for CC/BCC (Thomas Keitel) + * Added support for multiple body parts (Thomas Keitel) +1. [](#bugfix) + * Fix email engine selection (z38) + +# v1.0.0 +## 11/20/2015 + +1. [](#bugfix) + * Fix for issue with no body parameter specified + +# v0.2.1 +## 09/11/2015 + +1. [](#bugfix) + * Fix onFormProcessed event + +# v0.2.0 +## 08/11/2015 + +1. [](#improved) + * Disable `enable` in admin + +# v0.1.0 +## 08/04/2015 + +1. [](#new) + * ChangeLog started... diff --git a/plugins/email/LICENSE b/plugins/email/LICENSE new file mode 100644 index 0000000..0e788c6 --- /dev/null +++ b/plugins/email/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Grav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/email/README.md b/plugins/email/README.md new file mode 100644 index 0000000..794b2e6 --- /dev/null +++ b/plugins/email/README.md @@ -0,0 +1,502 @@ +# Grav Email Plugin + +The **email plugin** for [Grav](http://github.com/getgrav/grav) adds the ability to send email utilizing the `symfony/mailer` package. This is particularly useful for the **admin** and **login** plugins. + +> IMPORTANT: **Version 4.0** replaced the old deprecated **SwiftMailer** library with **Symfony/Mailer** package. This is a modern and well supported library that also has the capability to support 3rd party transport engines such as `SendGrid`, `MailJet`, `MailGun`, `MailChimp`, etc. This library should be backwards compatible with existing configurations, but please create an issue if you run into any problems. + +# Installation + +The email plugin is easy to install with GPM. + +``` +$ bin/gpm install email +``` + +# Configuration + +The plugin uses `sendmail` binary as the default mail engine. + +``` +enabled: true +from: +to: +mailer: + engine: sendmail + smtp: + server: localhost + port: 25 + encryption: none + user: + password: + sendmail: + bin: '/usr/sbin/sendmail -bs' +content_type: text/html +debug: false +``` + +You can configure the Email plugin by using the Admin plugin, navigating to the Plugins list and choosing `Email`. + +That's the easiest route. Or you can also alter the Plugin configuration by copying the `user/plugins/email/email.yaml` file into `user/config/plugins/email.yaml` and make your modifications there. + +The first setting you'd likely change is your `Email from` / `Email to` names and emails. + +Also, you'd likely want to setup a SMTP server instead of using PHP Mail, as the latter is not 100% reliable and you might experience problems with emails. + +## Built-in Engines + +By default Email 4.0 supports 4 native engines: + +* SMTP - Standard "Simple Mail Transport Protocol" - The default for most providers +* SMTPS - "Simple Mail Transport Protocol Secure" - Not very commonly used +* Sendmail - Uses the built-in `sendmail` binary file available on many Linux and Mac systems +* Native - Uses `sendmail_path` of `php.ini` for Mac + Linux, and `smtp` and `smtp_port` on Windows + +Due to the modular nature of Symfony/Mailer, 3rd party engines are supported via Grav plugins. + +## 3rd-Party Engines Plugin Support + +Along with the **Email** `v4.0` release, there has also been several custom provider plugins released to provide support for `SMTP`, `API`, and sometimes even `HTTPS` support for 3rd party providers such as **Sendgrid**, **MailJet**, **MailGun**, **Amazon SES**, **Mailchimp/Mandrill**, and others! `API` or `HTTPS` will provide a faster email sending experience compared to `SMTP` which is an older protocol and requires more back-and-forth negotiation and communication compared to the single-request of `API` or `HTTPS` solutions. + +Examples of the currently available plugins include: + +* https://github.com/getgrav/grav-plugin-email-sendgrid - Sengrid Mailer +* https://github.com/getgrav/grav-plugin-email-amazon - Amazon SES +* https://github.com/getgrav/grav-plugin-email-mandrill - Mailchimp Mandrill Mailer +* https://github.com/getgrav/grav-plugin-email-mailersend - Mailersend Mailer + +More plugins will be released soon to support `Gmail`, `Mailgun`, `Mailjet`, `OhMySMTP`, `Postmark`, and `SendInBlue`. + +## SMTP Configurations for popular solutions: + +### Google Email + +A popular option for sending email is to simply use your Google Accounts SMTP server. To set this up you will need to do 2 things first: + +As Gmail no longer supports the "allow less secure apps" option, you now need to have 2FA enabled on the account and setup an "App Password" to create a specific password rather than your general account password. Follow these instructions: [https://support.google.com/accounts/answer/185833](https://support.google.com/accounts/answer/185833) + +Then configure the Email plugin: + +``` +mailer: + engine: smtp + smtp: + server: smtp.gmail.com + port: 587 + user: 'YOUR_GOOGLE_EMAIL_ADDRESS' + password: 'YOUR_GOOGLE_PASSWORD' +``` + +> NOTE: Check your email sending limits: https://support.google.com/a/answer/166852?hl=en + +### Mailtrap.io + +A good way to test emails is to use a SMTP server service that's built for testing emails, for example [https://mailtrap.io](https://mailtrap.io) + +Setup the Email plugin to use that SMTP server with the fake inbox data. For example enter this configuration in `user/config/plugins/email.yaml` or through the Admin panel: + +``` +mailer: + engine: smtp + smtp: + server: smtp.mailtrap.io + port: 2525 + encryption: none + user: YOUR_MAILTRAP_INBOX_USER + password: YOUR_MAILTRAP_INBOX_PASSWORD +``` + +That service will intercept emails and show them on their web-based interface instead of sending them for real. + +You can try and fine tune the emails there while testing. + +### Sparkpost + +Generous email sending limits even in the free tier, and simple setup, make [Sparkpost](https://www.sparkpost.com) a great option for email sending. You just need to create an account, then setup a verified sending domain. Sparkpost does a nice job of making this process very easy and undertandable. Then just click on the SMTP Relay option to get your details for the configuration: + +``` +mailer: + engine: smtp + smtp: + server: smtp.sparkpostmail.com + port: 587 + user: 'SMTP_Injection' + password: 'SEND_EMAIL_API_KEY' +``` + +Then try sending a test email... + +### Sendgrid + +[Sendgrid](https://sendgrid.com) offers a very easy-to-setup serivce with 100 emails/day for free. The next level allows you to send 40k/email a day for just $10/month. Configuration is pretty simple, just create an account, then click SMTP integration and click the button to create an API key. The configuration is as follows: + +``` +mailer: + engine: smtp + smtp: + server: smtp.sendgrid.net + port: 587 + user: 'apikey' + password: 'YOUR_SENDGRID_API_KEY' +``` + +### Mailgun + +[Mailgun is a great service](https://www.mailgun.com/) that offers 10k/emails per month for free. Setup does require SPIF domain verification so that means you need to add at least a TXT entry in your DNS. This is pretty standard for SMTP sending services and does provide verification for remote email servers and makes your email sending more reliable. The Mailgun site, walks you through this process however, and the verification process is simple and fast. + +``` +mailer: + engine: smtp + smtp: + server: smtp.mailgun.org + port: 587 + user: 'MAILGUN_EMAIL_ADDRESS' + password: 'MAILGUN_EMAIL_PASSWORD' +``` + +Adjust these configurations for your account. + +### MailJet + +Mailjet is another great service that is easy to quickly setup and get started sending email. The free account gives you 200 emails/day or 600 emails/month. Just signup and setup your SPF and DKIM entries for your domain. Then click on the SMTP settings and use those to configure the email plugin: + +``` +mailer: + engine: smtp + smtp: + server: in-v3.mailjet.com + port: 587 + user: 'MAILJUST_USERNAME_API_KEY' + password: 'MAILJUST_PASSWORD_SECRET_KEY' +``` + +### ZOHO + +ZOHO is a popular solution for hosted email due to it's great 'FREE' tier. It's paid options are also very reasonable and combined with the latest UI updates and outstanding security features, it's a solid email option. + +In order to get ZOHO working with Grav, you need to send email via a user account. You can either use your users' password or generate an **App Password** via your ZOHO account (clicking on your avatar once logged in), then navigating to `My Account -> Security -> App Passwords -> Generate`. Just enter a unique app name (i.e. `Grav Website`). + +NOTE: The SMTP host required can be found in `Settings -> Mail - > Mail Accounts -> POP/IMAP -> SMTP`. This will provide the SMTP server for this account (it may not be `imap.zoho.com` depending on what region you are in) + +``` +mailer: + engine: smtp + smtp: + server: smtp.zoho.com + port: 587 + user: 'ZOHO_EMAIL_ADDRESS' + password: 'ZOHO_EMAIL_PASSWORD' +``` + +### Sendmail + +Although not as reliable as SMTP not providing as much debug information, sendmail is a simple option as long as your hosting provider is not blocking the default SMTP port `25`: + +``` +mailer: + engine: sendmail + sendmail: + bin: '/usr/sbin/sendmail -bs' +``` + +Simply adjust your binary command line to suite your environment + +## SMTP Email Services + +Solid SMTP options that even provide a FREE tier for low email volumes include: + +* SendGrid (100/day free) - https://sendgrid.com +* Mailgun - (10k/month free) - https://www.mailgun.com +* Mailjet - (6k/month free) - https://www.mailjet.com/ +* Sparkpost - (15k/month free) - https://www.sparkpost.com +* Amazon SES (62k/month free) - https://aws.amazon.com/ses/ + +If you are still unsure why should be using one in the first place, check out this article: https://zapier.com/learn/email-marketing/best-transactional-email-sending-services/ + +## Testing with CLI Command + +You can test your email configuration with the following CLI Command: + +``` +$ bin/plugin email test-email -t test@email.com +``` + +You can also pass in a configuration environment: + +``` +$ bin/plugin email test-email -t test@email.com --env=mysite.com +``` + +This will use the email configuration you have located in `user/mysite.com/config/plugins/email.yaml`. Read the docs to find out more about environment-based configuration: https://learn.getgrav.org/advanced/environment-config + +# Programmatically send emails + +Add this code in your plugins: + +```php + + $to = 'email@test.com'; + $from = 'email@test.com'; + + $subject = 'Test'; + $content = 'Test'; + + $message = $this->grav['Email']->message($subject, $content, 'text/html') + ->setFrom($from) + ->setTo($to); + + $sent = $this->grav['Email']->send($message); +``` + +# Emails sent with Forms + +When executing email actions during form processing, action parameters are inherited from the global configuration but may also be overridden on a per-action basis. + +``` +title: Custom form + +form: + name: custom_form + fields: + + # Any fields you'd like to add to the form: + # Their values may be referenced in email actions via '{{ form.value.FIELDNAME|e }}' + + process: + email: + subject: "[Custom form] {{ form.value.name|e }}" + body: "{% include 'forms/data.txt.twig' %}" + from: Custom Sender + to: Custom Recipient + process_markdown: true +``` + +## Multiple Emails + +You can send multiple emails by creating an array of emails under the `process: email:` option in the form: + +``` +title: Custom form + +form: + name: custom_form + fields: + + # Any fields you'd like to add to the form: + # Their values may be referenced in email actions via '{{ form.value.FIELDNAME|e }}' + + process: + email: + - + subject: "[Custom Email 1] {{ form.value.name|e }}" + body: "{% include 'forms/data.txt.twig' %}" + from: Site Owner + to: Recipient 1 + template: "email/base.html.twig" + - + subject: "[Custom Email 2] {{ form.value.name|e }}" + body: "{% include 'forms/data.txt.twig' %}" + from: Site Owner + to: Recipient 2 + template: "email/base.html.twig" +``` + +## Templating Emails + +You can specify a Twig template for HTML rendering, else Grav will use the default one `email/base.html.twig` which is included in this plugin. You can also specify a custom template that extends the base, where you can customize the `{% block content %}` and `{% block footer %}`. For example: + +```twig +{% extends 'email/base.html.twig' %} + +{% block content %} +

    + Greetings {{ form.value.name|e }}, +

    + +

    + We have received your request for help. Our team will get in touch with you within 3 Business Days. +

    + +

    + Regards, +

    + +

    + My Company +

    + E - help@mycompany.com
    + M - +1 555-123-4567
    + W - mycompany.com +

    +{% endblock %} + +{% block footer %} +

    My Company - All Rights Reserved

    +{% endblock %} +``` + +## Sending Attachments + +You can add file inputs to your form, and send those files via Email. +Just add an `attachments` field and list the file input fields names. You can have multiple file fields, and this will send all the files as attachments. Example: + +``` +form: + name: custom_form + fields: + + my-file: + label: 'Add a file' + type: file + multiple: false + destination: user/data/files + accept: + - application/pdf + - application/x-pdf + - image/png + - text/plain + + process: + + email: + body: '{% include "forms/data.html.twig" %}' + attachments: + - 'my-file' +``` + +## Additional action parameters + +To have more control over your generated email, you may also use the following additional parameters: + +* `reply_to`: Set one or more addresses that should be used to reply to the message. +* `cc` _(Carbon copy)_: Add one or more addresses to the delivery list. Many email clients will mark email in one's inbox differently depending on whether they are in the `To:` or `Cc:` list. +* `bcc` _(Blind carbon copy)_: Add one or more addresses to the delivery list that should (usually) not be listed in the message data, remaining invisible to other recipients. + +### Specifying email addresses + +Email-related parameters (`from`, `to`, `reply_to`, `cc`and `bcc`) allow different notations for single / multiple values: + +#### Single email address string + +``` +to: mail@example.com +``` + +#### `name-addr` RFC822 Formatted string + +``` +to: Joe Bloggs +``` + +#### Multiple email address strings + +``` +to: + - mail@example.com + - mail+1@example.com + - mail+2@example.com +``` + +or in `name-addr` format: + +``` +to: + - Joe Bloggs + - Jane Doe + - Jasper Jesperson +``` + +#### Simple array format with names + +``` +to: [mail@exmaple.com, Joe Bloggs] +``` + +#### Formatted email address with names + +``` +to: + email: mail@example.com + name: Joe Bloggs +``` + +or inline: + +``` +to: {email: 'mail@example.com', name: 'Joe Bloggs'} +``` + +#### Multiple email addresses (with and without names) + +``` +to: + - [mail@example.com, Joe Bloggs] + - [mail+2@example.com, Jane Doe] +``` + +``` +to: + - + email: mail@example.com + name: Joe Bloggs + - + email: mail+2@example.com + name: Jane Doe +``` + +or inline: + +``` +to: + - {email: 'mail@example.com', name: 'Joe Bloggs'} + - {email: 'mail+2@example.com', name: 'Jane Doe'} +``` + +## Multi-part MIME messages + +Apart from a simple string, an email body may contain different MIME parts (e.g. HTML body with plain text fallback): + +``` +body: + - + content_type: 'text/html' + body: "{% include 'forms/default/data.html.twig' %}" + - + content_type: 'text/plain' + body: "{% include 'forms/default/data.txt.twig' %}" + +``` + +# Troubleshooting + +## Emails are not sent + +#### Debugging + +The first step in determining why emails are not sent is to enable debugging. This can be done via the `user/config/email.yaml` file or via the plugin settings in the admin. Just enable this and then try sending an email again. Then inspect the `logs/email.log` file for potential problems. + +#### ISP Port 25 blocking + +By default, when sending via PHP or Sendmail the machine running the webserver will attempt to send mail using the SMTP protocol. This uses port `25` which is often blocked by ISPs to protected against spamming. You can determine if this port is blocked by running this command in your temrinal (mac/linux only): + +``` +(echo >/dev/tcp/localhost/25) &>/dev/null && echo "TCP port 25 opened" || echo "TCP port 25 closed" +``` + +If it's blocked there are ways to configure relays to different ports, but the simplest solution is to use SMTP for mail sending. + + +#### Exceptions + +If you get an exception when sending email but you cannot see what the error is, you need to enable more verbose exception messages. In the `user/config/system.yaml` file ensure your have the following configuration: + +``` +errors: + display: 1 + log: true +``` + +## Configuration Issues + +As explained above in the Configuration section, if you're using the default settings, set the Plugin configuration to use a SMTP server. It can be [Gmail](https://www.digitalocean.com/community/tutorials/how-to-use-google-s-smtp-server) or another SMTP server you have at your disposal. + +This is the first thing to check. The reason is that PHP Mail, the default system used by the Plugin, is not 100% reliable and emails might not arrive. diff --git a/plugins/email/blueprints.yaml b/plugins/email/blueprints.yaml new file mode 100644 index 0000000..68c89de --- /dev/null +++ b/plugins/email/blueprints.yaml @@ -0,0 +1,172 @@ +name: Email +slug: email +type: plugin +version: 4.0.1 +testing: false +description: Enables the emailing system for Grav +icon: envelope +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +keywords: plugin, email, sender +homepage: https://github.com/getgrav/grav-plugin-email +bugs: https://github.com/getgrav/grav-plugin-email/issues +license: MIT + +dependencies: + - { name: grav, version: '>=1.7.41' } + - { name: form, version: '>=7.0.0' } + +form: + validation: loose + + fields: + enabled: + type: hidden + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + mailer.engine: + type: select + label: PLUGIN_EMAIL.MAIL_ENGINE + size: medium + description: PLUGIN_EMAIL.MAIL_ENGINE_DESC + data-options@: '\Grav\Plugin\EmailPlugin::getEngines' + + smtp_config: + type: section + title: PLUGIN_EMAIL.SMTP_CONFIGURATION + underline: true + + mailer.smtp.server: + type: text + size: medium + label: PLUGIN_EMAIL.SMTP_SERVER + placeholder: PLUGIN_EMAIL.SMTP_SERVER_PLACEHOLDER + + mailer.smtp.port: + type: text + size: small + label: PLUGIN_EMAIL.SMTP_PORT + placeholder: PLUGIN_EMAIL.SMTP_PORT_PLACEHOLDER + validate: + type: number + min: 1 + max: 65535 + + mailer.smtp.encryption: + type: select + size: medium + label: PLUGIN_EMAIL.SMTP_ENCRYPTION + options: + none: PLUGIN_EMAIL.SMTP_ENCRYPTION_NONE + ssl: SSL + tls: TLS + + mailer.smtp.user: + type: text + size: medium + autocomplete: 'off' + label: PLUGIN_EMAIL.SMTP_LOGIN_NAME + + mailer.smtp.password: + type: password + size: medium + autocomplete: 'new-password' + label: PLUGIN_EMAIL.SMTP_PASSWORD + + sendmail_config: + type: section + title: PLUGIN_EMAIL.SENDMAIL_CONFIGURATION + underline: true + + mailer.sendmail.bin: + type: text + size: medium + label: PLUGIN_EMAIL.PATH_TO_SENDMAIL + placeholder: "/usr/sbin/sendmail" + + + + email_Defaults: + type: section + title: PLUGIN_EMAIL.EMAIL_DEFAULTS + underline: true + + content_type: + type: select + label: PLUGIN_EMAIL.CONTENT_TYPE + size: medium + default: 'text/html' + options: + 'text/plain': PLUGIN_EMAIL.CONTENT_TYPE_PLAIN_TEXT + 'text/html': HTML + + from: + type: text + size: large + label: PLUGIN_EMAIL.EMAIL_FORM + placeholder: PLUGIN_EMAIL.EMAIL_FORM_PLACEHOLDER + help: PLUGIN_EMAIL.EMAIL_FORMAT + validate: + required: true + + to: + type: text + size: large + label: PLUGIN_EMAIL.EMAIL_TO + placeholder: PLUGIN_EMAIL.EMAIL_TO_PLACEHOLDER + help: PLUGIN_EMAIL.EMAIL_FORMAT + validate: + required: true + + cc: + type: text + size: large + label: PLUGIN_EMAIL.EMAIL_CC + placeholder: PLUGIN_EMAIL.EMAIL_CC_PLACEHOLDER + help: PLUGIN_EMAIL.EMAIL_FORMAT + + bcc: + type: text + size: large + label: PLUGIN_EMAIL.EMAIL_BCC + placeholder: PLUGIN_EMAIL.EMAIL_BCC_PLACEHOLDER + help: PLUGIN_EMAIL.EMAIL_FORMAT + + reply_to: + type: text + size: large + label: PLUGIN_EMAIL.EMAIL_REPLY_TO + placeholder: PLUGIN_EMAIL.EMAIL_REPLY_TO_PLACEHOLDER + help: PLUGIN_EMAIL.EMAIL_FORMAT + + body: + type: textarea + size: large + rows: 10 + label: PLUGIN_EMAIL.EMAIL_BODY + placeholder: PLUGIN_EMAIL.EMAIL_BODY_PLACEHOLDER + + advanced_section: + type: section + title: PLUGIN_EMAIL.ADVANCED + underline: true + + debug: + type: toggle + label: PLUGIN_EMAIL.DEBUG + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/plugins/email/classes/Email.php b/plugins/email/classes/Email.php new file mode 100644 index 0000000..617bbee --- /dev/null +++ b/plugins/email/classes/Email.php @@ -0,0 +1,512 @@ +initMailer(); + $this->initLog(); + } + + /** + * Returns true if emails have been enabled in the system. + * + * @return bool + */ + public static function enabled(): bool + { + return Grav::instance()['config']->get('plugins.email.mailer.engine') !== 'none'; + } + + /** + * Returns true if debugging on emails has been enabled. + * + * @return bool + */ + public static function debug(): bool + { + return Grav::instance()['config']->get('plugins.email.debug') == 'true'; + } + + /** + * Creates an email message. + * + * @param string|null $subject + * @param string|null $body + * @param string|null $contentType + * @param string|null $charset @deprecated + * @return Message + */ + public function message(string $subject = null, string $body = null, string $contentType = null, string $charset = null): Message + { + $message = new Message(); + $message->subject($subject); + if ($contentType === 'text/html') { + $message->html($body); + } else { + $message->text($body); + } + + return $message; + } + + /** + * Send email. + * + * @param Message $message + * @param Envelope|null $envelope + * @return int + */ + public function send(Message $message, Envelope $envelope = null): int + { + $status = '🛑 '; + $sent_msg = null; + $debug = null; + + try { + $sent_msg = $this->transport->send($message->getEmail(), $envelope); + $return = 1; + $status = '✅'; + $debug = $sent_msg->getDebug(); + } catch (TransportExceptionInterface $e) { + $return = 0; + $status .= $e->getMessage(); + $debug = $e->getDebug(); + } + + if ($this->debug()) { + $log_msg = "Email sent to %s at %s -> %s\n%s"; + $to = $this->jsonifyRecipients($message->getEmail()->getTo()); + $msg = sprintf($log_msg, $to, date('Y-m-d H:i:s'), $status, $debug); + $this->log->addInfo($msg); + } + + return $return; + } + + /** + * Build e-mail message. + * + * @param array $params + * @param array $vars + * @return Message + */ + public function buildMessage(array $params, array $vars = []): Message + { + /** @var Twig $twig */ + $twig = Grav::instance()['twig']; + $twig->init(); + + /** @var Config $config */ + $config = Grav::instance()['config']; + + /** @var Language $language */ + $language = Grav::instance()['language']; + + // Create message object. + $message = new Message(); + $headers = $message->getEmail()->getHeaders(); + $email = $message->getEmail(); + + // Extend parameters with defaults. + $params += [ + 'bcc' => $config->get('plugins.email.bcc', []), + 'body' => $config->get('plugins.email.body', '{% include "forms/data.html.twig" %}'), + 'cc' => $config->get('plugins.email.cc', []), + 'cc_name' => $config->get('plugins.email.cc_name'), + 'charset' => $config->get('plugins.email.charset', 'utf-8'), + 'from' => $config->get('plugins.email.from'), + 'from_name' => $config->get('plugins.email.from_name'), + 'content_type' => $config->get('plugins.email.content_type', 'text/html'), + 'reply_to' => $config->get('plugins.email.reply_to', []), + 'reply_to_name' => $config->get('plugins.email.reply_to_name'), + 'subject' => !empty($vars['form']) && $vars['form'] instanceof FormInterface ? $vars['form']->page()->title() : null, + 'to' => $config->get('plugins.email.to'), + 'to_name' => $config->get('plugins.email.to_name'), + 'process_markdown' => false, + 'template' => false, + 'message' => $message + ]; + + if (!$params['to']) { + throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_TO_ADDRESS')); + } + if (!$params['from']) { + throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_FROM_ADDRESS')); + } + + + // make email configuration available to templates + $vars += [ + 'email' => $params, + ]; + + $params = $this->processParams($params, $vars); + + // Process parameters. + foreach ($params as $key => $value) { + switch ($key) { + case 'body': + if (is_string($value)) { + $this->processBody($message, $params, $vars, $twig, $value); + } elseif (is_array($value)) { + foreach ($value as $body_part) { + $params_part = $params; + if (isset($body_part['content_type'])) { + $params_part['content_type'] = $body_part['content_type']; + } + if (isset($body_part['template'])) { + $params_part['template'] = $body_part['template']; + } + if (isset($body_part['body'])) { + $this->processBody($message, $params_part, $vars, $twig, $body_part['body']); + } + } + } + break; + + case 'subject': + if ($value) { + $message->subject($language->translate($value)); + } + break; + + case 'to': + case 'from': + case 'cc': + case 'bcc': + case 'reply_to': + if ($recipients = $this->processRecipients($key, $params)) { + $key = $key === 'reply_to' ? 'replyTo' : $key; + $email->$key(...$recipients); + } + break; + case 'tags': + foreach ((array) $value as $tag) { + if (is_string($tag)) { + $headers->add(new TagHeader($tag)); + } + } + break; + case 'metadata': + foreach ((array) $value as $k => $v) { + if (is_string($k) && is_string($v)) { + $headers->add(new MetadataHeader($k, $v)); + } + } + break; + } + } + + return $message; + } + + /** + * @param string $type + * @param array $params + * @return array + */ + protected function processRecipients(string $type, array $params): array + { + $recipients = $params[$type] ?? Grav::instance()['config']->get('plugins.email.'.$type) ?? []; + + $list = []; + + if (!empty($recipients)) { + if (is_array($recipients) && Utils::isAssoc($recipients)) { + $list[] = $this->createAddress($recipients); + } else { + if (is_array($recipients)) { + if (count($recipients) ===2 && $this->isValidEmail($recipients[0]) && is_string($recipients[1])) { + $list[] = $this->createAddress($recipients); + } else { + foreach ($recipients as $recipient) { + $list[] = $this->createAddress($recipient); + } + } + } else { + if (is_string($recipients) && Utils::contains($recipients, ',')) { + $recipients = array_map('trim', explode(',', $recipients)); + foreach ($recipients as $recipient) { + $list[] = $this->createAddress($recipient); + } + } else { + if (!Utils::contains($recipients, ['<','>']) && ($params[$type."_name"])) { + $recipients = [$recipients, $params[$type."_name"]]; + } + $list[] = $this->createAddress($recipients); + } + } + } + } + + return $list; + } + + /** + * @param $data + * @return Address + */ + protected function createAddress($data): Address + { + if (is_string($data)) { + preg_match('/^(.*)\<(.*)\>$/', $data, $matches); + if (isset($matches[2])) { + $email = trim($matches[2]); + $name = trim($matches[1]); + } else { + $email = $data; + $name = ''; + } + } elseif (Utils::isAssoc($data)) { + $first_key = array_key_first($data); + if (filter_var($first_key, FILTER_VALIDATE_EMAIL)) { + $email = $first_key; + $name = $data[$first_key]; + } else { + $email = $data['email'] ?? $data['mail'] ?? $data['address'] ?? ''; + $name = $data['name'] ?? $data['fullname'] ?? ''; + } + } else { + $email = $data[0] ?? ''; + $name = $data[1] ?? ''; + } + return new Address($email, $name); + } + + /** + * @return null|Mailer + * @internal + */ + protected function initMailer(): ?Mailer + { + if (!$this->enabled()) { + return null; + } + if (!$this->mailer) { + $this->transport = $this->getTransport(); + // Create the Mailer using your created Transport + $this->mailer = new Mailer($this->transport); + } + return $this->mailer; + } + + /** + * @return void + * @throws \Exception + */ + protected function initLog() + { + $log_file = Grav::instance()['locator']->findResource('log://email.log', true, true); + $this->log = new Logger('email'); + /** @var UniformResourceLocator $locator */ + $this->log->pushHandler(new StreamHandler($log_file, Logger::DEBUG)); + } + + /** + * @param array $params + * @param array $vars + * @return array + */ + protected function processParams(array $params, array $vars = []): array + { + $twig = Grav::instance()['twig']; + array_walk_recursive($params, function(&$value) use ($twig, $vars) { + if (is_string($value)) { + $value = $twig->processString($value, $vars); + } + }); + return $params; + } + + /** + * @param $message + * @param $params + * @param $vars + * @param $twig + * @param $body + * @return void + */ + protected function processBody($message, $params, $vars, $twig, $body) + { + if ($params['process_markdown'] && $params['content_type'] === 'text/html') { + $body = (new Parsedown())->text($body); + } + + if ($params['template']) { + $body = $twig->processTemplate($params['template'], ['content' => $body] + $vars); + } + + $content_type = !empty($params['content_type']) ? $twig->processString($params['content_type'], $vars) : null; + + if ($content_type === 'text/html') { + $message->html($body); + } else { + $message->text($body); + } + } + + /** + * @return TransportInterface + */ + protected static function getTransport(): Transport\TransportInterface + { + /** @var Config $config */ + $config = Grav::instance()['config']; + $engine = $config->get('plugins.email.mailer.engine'); + $dsn = 'null://default'; + + + // Create the Transport and initialize it. + switch ($engine) { + case 'smtps': + case 'smtp': + $options = $config->get('plugins.email.mailer.smtp'); + $dsn = $engine . '://'; + $auth = ''; + + if (isset($options['encryption']) && $options['encryption'] === 'none') { + $options['options']['verify_peer'] = 0; + } + if (isset($options['user'])) { + $auth .= urlencode($options['user']); + } + if (isset($options['password'])) { + $auth .= ':'. urlencode($options['password']); + } + if (!empty($auth)) { + $dsn .= "$auth@"; + } + if (isset($options['server'])) { + $dsn .= urlencode($options['server']); + } + if (isset($options['port'])) { + $dsn .= ":{$options['port']}"; + } + if (isset($options['options'])) { + $dsn .= '?' . http_build_query($options['options']); + } + break; + case 'mail': + case 'native': + $dsn = 'native://default'; + break; + case 'sendmail': + $dsn = 'sendmail://default'; + $bin = $config->get('plugins.email.mailer.sendmail.bin'); + if (isset($bin)) { + $dsn .= '?command=' . urlencode($bin); + } + break; + default: + $e = new Event(['engine' => $engine, ]); + Grav::instance()->fireEvent('onEmailTransportDsn', $e); + if (isset($e['dsn'])) { + $dsn = $e['dsn']; + } + break; + } + + if ($dsn instanceof TransportInterface) { + $transport = $dsn; + } else { + $transport = Transport::fromDsn($dsn) ; + } + + return $transport; + } + + /** + * @param array $recipients + * @return string + */ + protected function jsonifyRecipients(array $recipients): string + { + $json = []; + foreach ($recipients as $recipient) { + $json[] = str_replace('"', "", $recipient->toString()); + } + return json_encode($json); + } + + protected function isValidEmail($email): bool + { + return is_string($email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * @return void + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + */ + public static function flushQueue() {} + + /** + * @return void + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + */ + public static function clearQueueFailures() {} + + /** + * Creates an attachment. + * + * @param string $data + * @param string $filename + * @param string $contentType + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + * @return void + */ + public function attachment($data = null, $filename = null, $contentType = null) {} + + /** + * Creates an embedded attachment. + * + * @param string $data + * @param string $filename + * @param string $contentType + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + * @return void + */ + public function embedded($data = null, $filename = null, $contentType = null) {} + + + /** + * Creates an image attachment. + * + * @param string $data + * @param string $filename + * @param string $contentType + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + * @return void + */ + public function image($data = null, $filename = null, $contentType = null) {} + +} diff --git a/plugins/email/classes/Message.php b/plugins/email/classes/Message.php new file mode 100644 index 0000000..fb691f8 --- /dev/null +++ b/plugins/email/classes/Message.php @@ -0,0 +1,97 @@ +email = new SymfonyEmail(); + } + + public function subject($subject): self + { + $this->email->subject($subject); + return $this; + } + + public function to($to): self + { + $this->email->to($to); + return $this; + } + + public function from($from): self + { + $this->email->from($from); + return $this; + } + + public function cc($cc): self + { + $this->email->cc($cc); + return $this; + } + + public function bcc($bcc): self + { + $this->email->bcc($bcc); + return $this; + } + + public function replyTo($reply_to): self + { + $this->email->replyTo($reply_to); + return $this; + } + + public function text($text): self + { + $this->email->text($text); + return $this; + } + + public function html($html): self + { + $this->email->html($html); + return $this; + } + + public function attachFromPath($path): self + { + $this->email->attachFromPath($path); + return $this; + } + + public function embedFromPath($path): self + { + $this->email->embedFromPath($path); + return $this; + } + + public function reply_to($reply_to): self + { + $this->replyTo($reply_to); + return $this; + } + + public function setFrom($from): self + { + $this->from($from); + return $this; + } + + public function setTo($to): self + { + $this->to($to); + return $this; + } + + public function getEmail(): SymfonyEmail + { + return $this->email; + } +} \ No newline at end of file diff --git a/plugins/email/classes/Utils.php b/plugins/email/classes/Utils.php new file mode 100644 index 0000000..69b85b4 --- /dev/null +++ b/plugins/email/classes/Utils.php @@ -0,0 +1,48 @@ + $params + * + * @return bool True if the action was performed. + */ + public static function sendEmail(...$params) + { + if (is_array($params[0])) { + $params = array_shift($params); + } else { + $keys = ['subject', 'body', 'to', 'from', 'content_type']; + $params = GravUtils::arrayCombine($keys, $params); + } + + //Initialize twig if not yet initialized + /** @var Twig $twig */ + $twig = Grav::instance()['twig']->init(); + + /** @var Email $email */ + $email = Grav::instance()['Email']; + + if (empty($params['to']) || empty($params['subject']) || empty($params['body'])) { + return false; + } + + $params['body'] = $twig->processTemplate('email/base.html.twig', ['content' => $params['body']]); + + $message = $email->buildMessage($params); + + return $email->send($message); + } +} diff --git a/plugins/email/cli/ClearQueueFailuresCommand.php b/plugins/email/cli/ClearQueueFailuresCommand.php new file mode 100644 index 0000000..42910c3 --- /dev/null +++ b/plugins/email/cli/ClearQueueFailuresCommand.php @@ -0,0 +1,39 @@ +setName('clear-queue-failures') + ->setAliases(['clearqueue']) + ->setDescription('DEPRECATED: Clears any queue failures that have accumulated') + ->setHelp('The clear-queue-failures command clears any queue failures that have accumulated'); + } + + /** + * @return int + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + */ + protected function serve() + { + return 0; + } +} diff --git a/plugins/email/cli/FlushQueueCommand.php b/plugins/email/cli/FlushQueueCommand.php new file mode 100644 index 0000000..c9a5d99 --- /dev/null +++ b/plugins/email/cli/FlushQueueCommand.php @@ -0,0 +1,39 @@ +setName('flush-queue') + ->setAliases(['flushqueue']) + ->setDescription('DEPRECATED: Flushes the email queue of any pending emails') + ->setHelp('The flush-queue command flushes the email queue of any pending emails'); + } + + /** + * @return int + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + */ + protected function serve() + { + return 0; + } +} diff --git a/plugins/email/cli/TestEmailCommand.php b/plugins/email/cli/TestEmailCommand.php new file mode 100644 index 0000000..2f2f4e8 --- /dev/null +++ b/plugins/email/cli/TestEmailCommand.php @@ -0,0 +1,105 @@ +setName('test-email') + ->setAliases(['testemail']) + ->addOption( + 'to', + 't', + InputOption::VALUE_REQUIRED, + 'An email address to send the email to' + ) + ->addOption( + 'env', + 'e', + InputOption::VALUE_OPTIONAL, + 'The environment to trigger a specific configuration. For example: localhost, mysite.dev, www.mysite.com' + ) + ->addOption( + 'subject', + 's', + InputOption::VALUE_OPTIONAL, + 'A subject for the email' + ) + ->addOption( + 'body', + 'b', + InputOption::VALUE_OPTIONAL, + 'The body of the email' + ) + ->setDescription('Sends a test email using the plugin\'s configuration') + ->setHelp('The test-email command sends a test email using the plugin\'s configuration'); + } + + /** + * @return int + */ + protected function serve() + { + // TODO: remove when requiring Grav 1.7+ + if (method_exists($this, 'initializeGrav')) { + $this->initializeThemes(); + } + + $this->output->writeln(''); + $this->output->writeln('Current Configuration:'); + $this->output->writeln(''); + + $grav = Grav::instance(); + $email_config = new Data($grav['config']->get('plugins.email')); + if ($email_config->get('mailer.smtp.password')) { + $password = $email_config->get('mailer.smtp.password'); + $obfuscated_password = str_repeat('*', strlen($password) - 2) . substr($password, -2); + $email_config->set('mailer.smtp.password', $obfuscated_password); + } + + dump($email_config); + + $this->output->writeln(''); + + $to = $this->input->getOption('to') ?: $grav['config']->get('plugins.email.to'); + $subject = $this->input->getOption('subject'); + $body = $this->input->getOption('body'); + + if (!$subject) { + $subject = 'Testing Grav Email Plugin'; + } + + if (!$body) { + $configuration = print_r($email_config, true); + $body = $grav['language']->translate(['PLUGIN_EMAIL.TEST_EMAIL_BODY', $configuration]); + } + + $sent = EmailUtils::sendEmail(['subject'=>$subject, 'body'=>$body, 'to'=>$to]); + + if ($sent) { + $this->output->writeln("Message sent successfully!"); + } else { + $this->output->writeln("Problem sending email..."); + } + + return 0; + } +} diff --git a/plugins/email/composer.json b/plugins/email/composer.json new file mode 100644 index 0000000..4928588 --- /dev/null +++ b/plugins/email/composer.json @@ -0,0 +1,44 @@ +{ + "name": "getgrav/grav-plugin-email", + "type": "grav-plugin", + "description": "Email plugin for Grav CMS", + "keywords": ["email", "plugin", "sender"], + "homepage": "https://github.com/getgrav/grav-plugin-email", + "license": "MIT", + "authors": [ + { + "name": "Team Grav", + "email": "devs@getgrav.org", + "homepage": "https://getgrav.org", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/getgrav/grav-plugin-email/issues", + "irc": "https://chat.getgrav.org", + "forum": "https://getgrav.org/forum", + "docs": "https://github.com/getgrav/grav-plugin-email/blob/master/README.md" + }, + "require": { + "php": ">=7.3.6", + "symfony/mailer": "^5.4", + "symfony/messenger": "^5.4" + }, + "replace": { + "symfony/polyfill-iconv": "*", + "symfony/polyfill-mbstring": "*", + "symfony/polyfill-php72": "*" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\Email\\": "classes/", + "Grav\\Plugin\\Console\\": "cli/" + }, + "classmap": ["email.php"] + }, + "config": { + "platform": { + "php": "7.3.6" + } + } +} diff --git a/plugins/email/composer.lock b/plugins/email/composer.lock new file mode 100644 index 0000000..f9b9152 --- /dev/null +++ b/plugins/email/composer.lock @@ -0,0 +1,1342 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "af2cff88b1d472f0931ba2c86fcdddc9", + "packages": [ + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, + { + "name": "egulias/email-validator", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f88dcf4b14af14a98ad96b14b2b317969eab6715", + "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.2", + "php": ">=7.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2022-06-18T20:57:19+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/amqp-messenger", + "version": "v5.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/amqp-messenger.git", + "reference": "def93f2a7841cfa1a4a1fa487b84054d0d53e521" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/amqp-messenger/zipball/def93f2a7841cfa1a4a1fa487b84054d0d53e521", + "reference": "def93f2a7841cfa1a4a1fa487b84054d0d53e521", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/messenger": "^5.3|^6.0" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony AMQP extension Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/amqp-messenger/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-09-11T09:11:59+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/doctrine-messenger", + "version": "v5.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-messenger.git", + "reference": "7649a80e917b47c5072480a2d763c2422da239d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/7649a80e917b47c5072480a2d763c2422da239d2", + "reference": "7649a80e917b47c5072480a2d763c2422da239d2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/messenger": "^5.1|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<2.13", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/dbal": "^2.13|^3.0", + "doctrine/persistence": "^1.3|^2|^3", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Doctrine Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-messenger/tree/v5.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-08-09T12:54:00+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-05T16:45:39+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/mailer", + "version": "v5.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11", + "reference": "63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3", + "php": ">=7.2.5", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.2.6|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "require-dev": { + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/messenger": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-08-29T06:47:07+00:00" + }, + { + "name": "symfony/messenger", + "version": "v5.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "8f8d3425991e627902f8288088609a8d8f6c6ea4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/8f8d3425991e627902f8288088609a8d8f6c6ea4", + "reference": "8f8d3425991e627902f8288088609a8d8f6c6ea4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/log": "^1|^2|^3", + "symfony/amqp-messenger": "^5.1|^6.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/doctrine-messenger": "^5.1|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/redis-messenger": "^5.1|^6.0" + }, + "conflict": { + "symfony/event-dispatcher": "<4.4", + "symfony/framework-bundle": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/serializer": "<5.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/serializer": "^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/validator": "^4.4|^5.0|^6.0" + }, + "suggest": { + "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-09-17T07:31:22+00:00" + }, + { + "name": "symfony/mime", + "version": "v5.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd", + "reference": "bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.1|^6.0", + "symfony/property-info": "^4.4|^5.1|^6.0", + "symfony/serializer": "^5.2|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-09-01T18:18:29+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-10T07:21:04+00:00" + }, + { + "name": "symfony/redis-messenger", + "version": "v5.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/redis-messenger.git", + "reference": "d3028b772de91e9aa0342c92ff71c77b130ac9c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/redis-messenger/zipball/d3028b772de91e9aa0342c92ff71c77b130ac9c4", + "reference": "d3028b772de91e9aa0342c92ff71c77b130ac9c4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/messenger": "^5.1|^6.0" + }, + "require-dev": { + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Redis\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Redis extension Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/redis-messenger/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-09-11T09:11:59+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-30T19:17:29+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.3.6" + }, + "platform-dev": [], + "platform-overrides": { + "php": "7.3.6" + }, + "plugin-api-version": "2.3.0" +} diff --git a/plugins/email/email.php b/plugins/email/email.php new file mode 100644 index 0000000..b96ff36 --- /dev/null +++ b/plugins/email/email.php @@ -0,0 +1,187 @@ + ['onPluginsInitialized', 0], + 'onFormProcessed' => ['onFormProcessed', 0], + 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], + 'onSchedulerInitialized' => ['onSchedulerInitialized', 0], + 'onAdminSave' => ['onAdminSave', 0], + ]; + } + + /** + * @return ClassLoader + */ + public function autoload(): ClassLoader + { + return require __DIR__ . '/vendor/autoload.php'; + } + + /** + * Initialize emailing. + */ + public function onPluginsInitialized() + { + $this->email = new Email(); + + if ($this->email::enabled()) { + $this->grav['Email'] = $this->email; + } + } + + /** + * Add twig paths to plugin templates. + */ + public function onTwigTemplatePaths() + { + $twig = $this->grav['twig']; + $twig->twig_paths[] = __DIR__ . '/templates'; + } + + /** + * Force compile during save if admin plugin save + * + * @param Event $event + */ + public function onAdminSave(Event $event) + { + /** @var Data $obj */ + $obj = $event['object']; + + if ($obj instanceof Data && $obj->blueprints()->getFilename() === 'email/blueprints') { + $current_pw = $this->grav['config']->get('plugins.email.mailer.smtp.password'); + $new_pw = $obj->get('mailer.smtp.password'); + if (!empty($current_pw) && empty($new_pw)) { + $obj->set('mailer.smtp.password', $current_pw); + } + + } + } + + /** + * Send email when processing the form data. + * + * @param Event $event + */ + public function onFormProcessed(Event $event) + { + $form = $event['form']; + $action = $event['action']; + $params = $event['params']; + + if (!$this->email->enabled()) { + return; + } + + switch ($action) { + case 'email': + // Prepare Twig variables + $vars = array( + 'form' => $form, + 'page' => $this->grav['page'] + ); + + // Copy files now, we need those. + // TODO: needs an update + $form->legacyUploads(); + $form->copyFiles(); + + $this->grav->fireEvent('onEmailSend', new Event(['params' => &$params, 'vars' => &$vars])); + + if (Utils::isAssoc($params)) { + $this->sendFormEmail($form, $params, $vars); + } else { + foreach ($params as $email) { + $this->sendFormEmail($form, $email, $vars); + } + } + + break; + } + } + + protected function sendFormEmail($form, $params, $vars) + { + // Build message + $message = $this->email->buildMessage($params, $vars); + + if (isset($params['attachments'])) { + $filesToAttach = (array)$params['attachments']; + if ($filesToAttach) foreach ($filesToAttach as $fileToAttach) { + $filesValues = $form->value($fileToAttach); + + if ($filesValues) foreach($filesValues as $fileValues) { + if (isset($fileValues['file'])) { + $filename = $fileValues['file']; + } else { + $filename = ROOT_DIR . $fileValues['path']; + } + + try { + $message->attachFromPath($filename); + } catch (\Exception $e) { + // Log any issues + $this->grav['log']->error($e->getMessage()); + } + } + } + } + + //fire event to apply optional signers + $this->grav->fireEvent('onEmailMessage', new Event(['message' => $message, 'params' => $params, 'form' => $form])); + + // Send e-mail + $this->email->send($message); + + //fire event after eMail was sent + $this->grav->fireEvent('onEmailSent', new Event(['message' => $message, 'params' => $params, 'form' => $form])); + } + + /** + * Used for dynamic blueprint field + * + * @return array + */ + public static function getEngines(): array + { + $engines = (object) [ + 'sendmail' => 'Sendmail', + 'smtp' => 'SMTP', + 'smtps' => 'SMTPS', + 'native' => 'Native', + 'none' => 'PLUGIN_ADMIN.DISABLED', + ]; + Grav::instance()->fireEvent('onEmailEngines', new Event(['engines' => $engines])); + return (array) $engines; + } + + /** + * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported + */ + public function onSchedulerInitialized(Event $e) + { + + } + +} diff --git a/plugins/email/email.yaml b/plugins/email/email.yaml new file mode 100644 index 0000000..846ae77 --- /dev/null +++ b/plugins/email/email.yaml @@ -0,0 +1,15 @@ +enabled: true +from: +to: +mailer: + engine: sendmail + smtp: + server: localhost + port: 25 + encryption: none + user: + password: + sendmail: + bin: '/usr/sbin/sendmail -bs' +content_type: text/html +debug: false \ No newline at end of file diff --git a/plugins/email/hebe.json b/plugins/email/hebe.json new file mode 100644 index 0000000..3831af4 --- /dev/null +++ b/plugins/email/hebe.json @@ -0,0 +1,15 @@ +{ + "project":"grav-plugin-email", + "platforms":{ + "grav":{ + "nodes":{ + "plugin":[ + { + "source":"/", + "destination":"/user/plugins/email" + } + ] + } + } + } +} diff --git a/plugins/email/languages.yaml b/plugins/email/languages.yaml new file mode 100644 index 0000000..4cb044c --- /dev/null +++ b/plugins/email/languages.yaml @@ -0,0 +1,215 @@ +en: + PLUGIN_EMAIL: + MAIL_ENGINE: "Mail Engine" + MAIL_ENGINE_DISABLED: "Disabled" + MAIL_ENGINE_DESC: "NOTE: If you select an engine provided by another plugin, you should configure options in that plugin." + CONTENT_TYPE: "Content type" + CONTENT_TYPE_PLAIN_TEXT: "Plain text" + CHARSET: "Charset" + CHARSET_PLACEHOLDER: "Defaults to UTF-8" + EMAIL_FORM: "From address" + EMAIL_FORM_PLACEHOLDER: "Default email from address" + EMAIL_FROM_NAME: "From name" + EMAIL_FROM_NAME_PLACEHOLDER: "Default email from name" + EMAIL_TO: "To address" + EMAIL_TO_PLACEHOLDER: "Default email to address" + EMAIL_TO_NAME: "To name" + EMAIL_TO_NAME_PLACEHOLDER: "Default email to name" + EMAIL_CC: "CC address" + EMAIL_CC_PLACEHOLDER: "Default email CC address" + EMAIL_CC_NAME: "CC name" + EMAIL_CC_NAME_PLACEHOLDER: "Default email CC name" + EMAIL_BCC: "BCC address" + EMAIL_BCC_PLACEHOLDER: "Default email BCC address" + EMAIL_REPLY_TO: "Reply-to address" + EMAIL_REPLY_TO_PLACEHOLDER: "Default email reply-to address" + EMAIL_REPLY_TO_NAME: "Reply-to name" + EMAIL_REPLY_TO_NAME_PLACEHOLDER: "Default email reply-to name" + EMAIL_BODY: "Body" + EMAIL_BODY_PLACEHOLDER: "Defaults to a table of all form fields" + SMTP_SERVER: "SMTP server" + SMTP_SERVER_PLACEHOLDER: "e.g. smtp.google.com" + SMTP_PORT: "SMTP port" + SMTP_PORT_PLACEHOLDER: "Defaults to 25 (plaintext) / 587 (encrypted)" + SMTP_ENCRYPTION: "SMTP encryption" + SMTP_ENCRYPTION_NONE: "None" + SMTP_LOGIN_NAME: "SMTP login name" + SMTP_PASSWORD: "SMTP password" + SMTP_AUTH_MODE: "SMTP auth mode" + PATH_TO_SENDMAIL: "Path to sendmail binary" + DEBUG: "Debug" + EMAIL_NOT_CONFIGURED: "Email not configured" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Please configure a 'to' address in the Email Plugin settings, or in the form" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Please configure a 'from' address in the Email Plugin settings, or in the form" + TEST_EMAIL_BODY: "

    Testing Email

    This test email has been sent based on the following configuration:

    %1$s

    " + EMAIL_DEFAULTS: "Email Defaults" + SMTP_CONFIGURATION: "SMTP/S Configuration" + SENDMAIL_CONFIGURATION: "Sendmail Configuration" + ADVANCED: "Advanced" + EMAIL_FOOTER: "GetGrav.org" + QUEUE_TITLE: "Email Queue" + QUEUE_DESC: "When you enable the email queue, email is not sent immediately, rather the email is sent to the queue, and then the Grav scheduler will flush the queue and actually send the email based on the configured frequency. This ensures Grav is not waiting around for email connections to complete." + QUEUE_ENABLED: "Enabled" + QUEUE_FLUSH_FREQUENCY: "Flush Frequency" + QUEUE_FLUSH_FREQUENCY_HELP: "Use 'cron' format" + QUEUE_FLUSH_MSG_LIMIT: "Messages per flush" + QUEUE_FLUSH_MSG_LIMIT_APPEND: "Messages" + QUEUE_FLUSH_TIME_LIMIT: "Flush time limit" + QUEUE_FLUSH_TIME_LIMIT_APPEND: "Seconds" + EMAIL_FORMAT: "Use the `addr` format: `email@address.org` or `name-addr` format: `Your Name `. Comma separated for multiple addresses" + +da: + PLUGIN_EMAIL: + PLEASE_CONFIGURE_A_TO_ADDRESS: "Konfigurere venligst en 'til' email adresse i Email Plugin indstillingerne eller her i formularen" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Konfigurere venligst en 'fra' email adresse i Email Plugin indstillingerne eller her i formularen" + +de: + PLUGIN_EMAIL: + EMAIL_NOT_CONFIGURED: "E-Mail ist nicht konfiguriert" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Bitte konfigurieren sie eine 'An' ('to') Adresse in den Email-Plugin-Einstellungen oder im Formular." + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Bitte konfigurieren sie eine 'Von' ('from') Adresse in den Email-Plugin-Einstellungen oder im Formular." + +es: + PLUGIN_EMAIL: + PLEASE_CONFIGURE_A_TO_ADDRESS: "Por favor configura una dirección de 'remitente' en la configuración del Plugin de Email o en el formulario" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Por favor configura una dirección de 'destinatario' en la configuración del Plugin de Email o en el formulario" + +fr: + PLUGIN_EMAIL: + MAIL_ENGINE: "Moteur de messagerie" + MAIL_ENGINE_DISABLED: "Désactivé" + CONTENT_TYPE: "Type de contenu" + CONTENT_TYPE_PLAIN_TEXT: "Texte brut" + CHARSET: "Jeu de caractères" + CHARSET_PLACEHOLDER: "Defaults to UTF-8" + EMAIL_FORM: "Adresse de l’expéditeur" + EMAIL_FORM_PLACEHOLDER: "Adresse e-mail par défaut de l’expéditeur" + EMAIL_FROM_NAME: "Nom de l’expéditeur" + EMAIL_FROM_NAME_PLACEHOLDER: "Nom par défaut de l’expéditeur" + EMAIL_TO: "Adresse du destinataire" + EMAIL_TO_PLACEHOLDER: "Adresse e-mail par défaut du destinataire" + EMAIL_TO_NAME: "Nom du destinataire" + EMAIL_TO_NAME_PLACEHOLDER: "Nom par défaut du destinataire" + EMAIL_CC: "Adresse en CC" + EMAIL_CC_PLACEHOLDER: "Adresse e-mail par défaut en CC" + EMAIL_CC_NAME: "Nom en CC" + EMAIL_CC_NAME_PLACEHOLDER: "Nom par défaut en CC" + EMAIL_BCC: "Adresse en BCC" + EMAIL_BCC_PLACEHOLDER: "Adresse e-mail par défaut en BCC" + EMAIL_REPLY_TO: "Adresse de réponse" + EMAIL_REPLY_TO_PLACEHOLDER: "Adresse e-mail par défaut pour la réponse" + EMAIL_REPLY_TO_NAME: "Nom pour la réponse" + EMAIL_REPLY_TO_NAME_PLACEHOLDER: "Nom par défaut pour la réponse" + EMAIL_BODY: "Corps" + EMAIL_BODY_PLACEHOLDER: "Table de tous les champs de formulaire par défaut" + SMTP_SERVER: "Serveur SMTP" + SMTP_SERVER_PLACEHOLDER: "ex. smtp.google.com" + SMTP_PORT: "Port SMTP" + SMTP_PORT_PLACEHOLDER: "25 (texte brut) / 587 (encrypté) par défaut" + SMTP_ENCRYPTION: "cryptage SMTP" + SMTP_ENCRYPTION_NONE: "Aucun" + SMTP_LOGIN_NAME: "Nom d'utilisateur SMTP" + SMTP_PASSWORD: "Mot de passe SMTP" + SMTP_AUTH_MODE: "Mode d'authentification SMTP" + PATH_TO_SENDMAIL: "Chemin vers sendmail" + DEBUG: "Débogage" + EMAIL_NOT_CONFIGURED: "L’e-mail n’est pas configuré" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Veuillez configurer une adresse de 'destinataire' dans les paramètres du plugin d’e-mail ou dans le formulaire." + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Veuillez configurer une adresse 'd’expéditeur' dans les paramètres du plugin d’e-mail ou dans le formulaire." + TEST_EMAIL_BODY: "

    E-mail de test

    Cet e-mail de test est basé sur la configuration suivante :

    %1$s

    " + EMAIL_DEFAULTS: "E-mail par défaut" + SMTP_CONFIGURATION: "Configuration SMTP" + SENDMAIL_CONFIGURATION: "Configuration Sendmail" + ADVANCED: "Avancée" + EMAIL_FOOTER: "GetGrav.org" + QUEUE_TITLE: "Fil d’attente d’e-mails" + QUEUE_DESC: "Lorsque vous activez la file d’attente des e-mails, ils ne sont pas envoyés immédiatement. Au contraire, ils sont envoyés dans la file d’attente, puis le planificateur Grav videra la file d’attente et enverra les e-mails en fonction de la fréquence configurée. Cela permet de s’assurer que Grav n’attend pas la fin des connexions pour les envoyer" + QUEUE_ENABLED: "Activé" + QUEUE_FLUSH_FREQUENCY: "Fréquence de Flush" + QUEUE_FLUSH_FREQUENCY_HELP: "Utiliser le format 'cron'" + QUEUE_FLUSH_MSG_LIMIT: "Messages par Flush" + QUEUE_FLUSH_MSG_LIMIT_APPEND: "Messages" + QUEUE_FLUSH_TIME_LIMIT: "Délai de Flush" + QUEUE_FLUSH_TIME_LIMIT_APPEND: "Seconds" + +hr: + PLUGIN_EMAIL: + EMAIL_NOT_CONFIGURED: "Email nije konfiguriran" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Konfigurirajte 'za' ('to') adresu u postavkama Email dodatka ili u obrascu" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Konfigurirajte 'od' ('from') adresu u postavkama Email dodatka ili u obrascu" + +it: + PLUGIN_EMAIL: + PLEASE_CONFIGURE_A_TO_ADDRESS: "Per favore, configura l'indirizzo di destinazione ('to') nella configurazione del Plugin Email, oppure direttamente nella form." + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Per favore, configura l'indirizzo di provenienza ('from') nella configurazione del Plugin Email, oppure direttamente nella form" + +ro: + PLUGIN_EMAIL: + EMAIL_NOT_CONFIGURED: "Adresa de email nu este configurată" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Vă rugam setați o adresă 'către' în setările modulului Email sau în formular" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Vă rugam setați o adresă 'de la' în setările modulului Email sau în formular" + +ru: + PLUGIN_EMAIL: + MAIL_ENGINE: "Почтовая система" + MAIL_ENGINE_DISABLED: "Отключена" + CONTENT_TYPE: "Тип контента" + CONTENT_TYPE_PLAIN_TEXT: "Простой текст" + CHARSET: "Кодировка" + CHARSET_PLACEHOLDER: "По умолчанию UTF-8" + EMAIL_DEFAULTS: "Email настройки по умолчанию" + EMAIL_FORM: "Почта отправителя" + EMAIL_FORM_PLACEHOLDER: "Email адрес отправителя по умолчанию" + EMAIL_FROM_NAME: "Имя почты отправителя" + EMAIL_FROM_NAME_PLACEHOLDER: "Email имя отправителя по умолчанию" + EMAIL_TO: "Почта получателя" + EMAIL_TO_PLACEHOLDER: "Email адрес получателя по умолчанию" + EMAIL_TO_NAME: "Имя почты получателя" + EMAIL_TO_NAME_PLACEHOLDER: "Email имя получателя по умолчанию" + EMAIL_CC: "Почта CC" + EMAIL_CC_PLACEHOLDER: "Email CC адрес по умолчанию" + EMAIL_CC_NAME: "Имя почты CC" + EMAIL_CC_NAME_PLACEHOLDER: "Email CC имя по умолчанию" + EMAIL_BCC: "Почта BCC" + EMAIL_BCC_PLACEHOLDER: "Email BCC адрес по умолчанию" + EMAIL_REPLY_TO: "Почта для ответов" + EMAIL_REPLY_TO_PLACEHOLDER: "Email для ответов адрес по умолчанию" + EMAIL_REPLY_TO_NAME: "Имя почты для ответов" + EMAIL_REPLY_TO_NAME_PLACEHOLDER: "Email для ответов имя по умолчанию" + EMAIL_BODY: "Тело сообщения" + EMAIL_BODY_PLACEHOLDER: "По умолчанию используется таблица всех полей формы" + SMTP_CONFIGURATION: "Конфигурация SMTP" + SMTP_SERVER: "SMTP сервер" + SMTP_SERVER_PLACEHOLDER: "e.g. smtp.google.com" + SMTP_PORT: "SMTP порт" + SMTP_PORT_PLACEHOLDER: "По умолчанию 25 (plaintext) / 587 (encrypted)" + SMTP_ENCRYPTION: "SMTP шифрование" + SMTP_ENCRYPTION_NONE: "Нет" + SMTP_LOGIN_NAME: "SMTP логин" + SMTP_PASSWORD: "SMTP пароль" + SENDMAIL_CONFIGURATION: "Конфигурация Sendmail" + PATH_TO_SENDMAIL: "Путь к sendmail" + QUEUE_TITLE: "Очередь Email" + QUEUE_DESC: "Когда вы включаете очередь email, электронная почта не отправляется немедленно, а отправляется в очередь, затем планировщик Grav обрабатывает очередь и на основе настроенной частоты фактически отправляет электронную почту. Это гарантирует, что Grav не ждет завершения подключения к электронной почте." + QUEUE_ENABLED: "Включено" + QUEUE_FLUSH_FREQUENCY: "Частота обработки" + QUEUE_FLUSH_FREQUENCY_HELP: "Использовать формат 'cron'" + QUEUE_FLUSH_MSG_LIMIT: "Количество сообщений на задачу" + QUEUE_FLUSH_MSG_LIMIT_APPEND: "Сообщений" + QUEUE_FLUSH_TIME_LIMIT: "Ограничение времени обработки" + QUEUE_FLUSH_TIME_LIMIT_APPEND: "Секунд" + ADVANCED: "Расширенные" + DEBUG: "Отладка" + EMAIL_NOT_CONFIGURED: "Электронная почта не настроена" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Пожалуйста настройте адрес получателя ('to') в настройках плагина Email Plugin, или на форме" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Пожалуйста настройте адрес отправителя ('from') в настройках плагина Email Plugin, или на форме" + TEST_EMAIL_BODY: "

    Тестирование электронной почты

    Это тестовое письмо отправлено на основе следующей конфигурации:

    %1$s

    " + EMAIL_FOOTER: "GetGrav.org" + +uk: + PLUGIN_EMAIL: + EMAIL_NOT_CONFIGURED: "Електронна пошта не налаштована" + PLEASE_CONFIGURE_A_TO_ADDRESS: "Будь ласка налаштуйте адресу одержувача ('to') в налаштуваннях плагіна Email Plugin, або на формі" + PLEASE_CONFIGURE_A_FROM_ADDRESS: "Будь ласка налаштуйте адресу відправника ('from') в налаштуваннях плагіна Email Plugin, або на формі" + TEST_EMAIL_BODY: "

    Тестування електронної пошти

    Це тестовий лист відправлено на основі такої конфігурації:

    %1$s

    " + EMAIL_FOOTER: "GetGrav.org" diff --git a/plugins/email/templates/email/base.html.twig b/plugins/email/templates/email/base.html.twig new file mode 100644 index 0000000..0dd827f --- /dev/null +++ b/plugins/email/templates/email/base.html.twig @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + +
    +
    + + + + +
    + {% block content %} + {{ content|raw }} + {% endblock content %} +
    +
    +
    + + + + + + + + + + + + + + diff --git a/plugins/email/vendor/autoload.php b/plugins/email/vendor/autoload.php new file mode 100644 index 0000000..e3e38f1 --- /dev/null +++ b/plugins/email/vendor/autoload.php @@ -0,0 +1,12 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var ?string */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-return array + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + * @private + */ +function includeFile($file) +{ + include $file; +} diff --git a/plugins/email/vendor/composer/InstalledVersions.php b/plugins/email/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..c6b54af --- /dev/null +++ b/plugins/email/vendor/composer/InstalledVersions.php @@ -0,0 +1,352 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints($constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = require __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + $installed[] = self::$installed; + + return $installed; + } +} diff --git a/plugins/email/vendor/composer/LICENSE b/plugins/email/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/plugins/email/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/email/vendor/composer/autoload_classmap.php b/plugins/email/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..ceabe59 --- /dev/null +++ b/plugins/email/vendor/composer/autoload_classmap.php @@ -0,0 +1,17 @@ + $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'Grav\\Plugin\\EmailPlugin' => $baseDir . '/email.php', + 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', +); diff --git a/plugins/email/vendor/composer/autoload_files.php b/plugins/email/vendor/composer/autoload_files.php new file mode 100644 index 0000000..d010da8 --- /dev/null +++ b/plugins/email/vendor/composer/autoload_files.php @@ -0,0 +1,13 @@ + $vendorDir . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', + 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', +); diff --git a/plugins/email/vendor/composer/autoload_namespaces.php b/plugins/email/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/plugins/email/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/symfony/polyfill-php80'), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), + 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), + 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), + 'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'), + 'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'), + 'Symfony\\Component\\Messenger\\Bridge\\Redis\\' => array($vendorDir . '/symfony/redis-messenger'), + 'Symfony\\Component\\Messenger\\Bridge\\Doctrine\\' => array($vendorDir . '/symfony/doctrine-messenger'), + 'Symfony\\Component\\Messenger\\Bridge\\Amqp\\' => array($vendorDir . '/symfony/amqp-messenger'), + 'Symfony\\Component\\Messenger\\' => array($vendorDir . '/symfony/messenger'), + 'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'), + 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), + 'Grav\\Plugin\\Email\\' => array($baseDir . '/classes'), + 'Grav\\Plugin\\Console\\' => array($baseDir . '/cli'), + 'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'), + 'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib/Doctrine/Common/Lexer'), +); diff --git a/plugins/email/vendor/composer/autoload_real.php b/plugins/email/vendor/composer/autoload_real.php new file mode 100644 index 0000000..850a062 --- /dev/null +++ b/plugins/email/vendor/composer/autoload_real.php @@ -0,0 +1,57 @@ +register(true); + + $includeFiles = \Composer\Autoload\ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$files; + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequire73924571ea6ee98bb12d10ff20aff2ab($fileIdentifier, $file); + } + + return $loader; + } +} + +/** + * @param string $fileIdentifier + * @param string $file + * @return void + */ +function composerRequire73924571ea6ee98bb12d10ff20aff2ab($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } +} diff --git a/plugins/email/vendor/composer/autoload_static.php b/plugins/email/vendor/composer/autoload_static.php new file mode 100644 index 0000000..0d0f4c2 --- /dev/null +++ b/plugins/email/vendor/composer/autoload_static.php @@ -0,0 +1,152 @@ + __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', + 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'Symfony\\Polyfill\\Php80\\' => 23, + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, + 'Symfony\\Polyfill\\Intl\\Idn\\' => 26, + 'Symfony\\Contracts\\Service\\' => 26, + 'Symfony\\Contracts\\EventDispatcher\\' => 34, + 'Symfony\\Component\\Mime\\' => 23, + 'Symfony\\Component\\Messenger\\Bridge\\Redis\\' => 41, + 'Symfony\\Component\\Messenger\\Bridge\\Doctrine\\' => 44, + 'Symfony\\Component\\Messenger\\Bridge\\Amqp\\' => 40, + 'Symfony\\Component\\Messenger\\' => 28, + 'Symfony\\Component\\Mailer\\' => 25, + 'Symfony\\Component\\EventDispatcher\\' => 34, + ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + 'Psr\\EventDispatcher\\' => 20, + 'Psr\\Container\\' => 14, + ), + 'G' => + array ( + 'Grav\\Plugin\\Email\\' => 18, + 'Grav\\Plugin\\Console\\' => 20, + ), + 'E' => + array ( + 'Egulias\\EmailValidator\\' => 23, + ), + 'D' => + array ( + 'Doctrine\\Common\\Lexer\\' => 22, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', + ), + 'Symfony\\Polyfill\\Intl\\Idn\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', + ), + 'Symfony\\Contracts\\Service\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/service-contracts', + ), + 'Symfony\\Contracts\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts', + ), + 'Symfony\\Component\\Mime\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/mime', + ), + 'Symfony\\Component\\Messenger\\Bridge\\Redis\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/redis-messenger', + ), + 'Symfony\\Component\\Messenger\\Bridge\\Doctrine\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/doctrine-messenger', + ), + 'Symfony\\Component\\Messenger\\Bridge\\Amqp\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/amqp-messenger', + ), + 'Symfony\\Component\\Messenger\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/messenger', + ), + 'Symfony\\Component\\Mailer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/mailer', + ), + 'Symfony\\Component\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/event-dispatcher', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + ), + 'Psr\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/event-dispatcher/src', + ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), + 'Grav\\Plugin\\Email\\' => + array ( + 0 => __DIR__ . '/../..' . '/classes', + ), + 'Grav\\Plugin\\Console\\' => + array ( + 0 => __DIR__ . '/../..' . '/cli', + ), + 'Egulias\\EmailValidator\\' => + array ( + 0 => __DIR__ . '/..' . '/egulias/email-validator/src', + ), + 'Doctrine\\Common\\Lexer\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/lexer/lib/Doctrine/Common/Lexer', + ), + ); + + public static $classMap = array ( + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'Grav\\Plugin\\EmailPlugin' => __DIR__ . '/../..' . '/email.php', + 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/plugins/email/vendor/composer/installed.json b/plugins/email/vendor/composer/installed.json new file mode 100644 index 0000000..b64a77e --- /dev/null +++ b/plugins/email/vendor/composer/installed.json @@ -0,0 +1,1378 @@ +{ + "packages": [ + { + "name": "doctrine/lexer", + "version": "1.2.3", + "version_normalized": "1.2.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "time": "2022-02-28T11:07:21+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "install-path": "../doctrine/lexer" + }, + { + "name": "egulias/email-validator", + "version": "3.2.1", + "version_normalized": "3.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f88dcf4b14af14a98ad96b14b2b317969eab6715", + "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.2", + "php": ">=7.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "time": "2022-06-18T20:57:19+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "install-path": "../egulias/email-validator" + }, + { + "name": "psr/container", + "version": "1.1.1", + "version_normalized": "1.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "time": "2021-03-05T17:36:06+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "install-path": "../psr/container" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "time": "2019-01-08T18:20:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "install-path": "../psr/event-dispatcher" + }, + { + "name": "psr/log", + "version": "1.1.4", + "version_normalized": "1.1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2021-05-03T11:20:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "install-path": "../psr/log" + }, + { + "name": "symfony/amqp-messenger", + "version": "v5.4.13", + "version_normalized": "5.4.13.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/amqp-messenger.git", + "reference": "def93f2a7841cfa1a4a1fa487b84054d0d53e521" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/amqp-messenger/zipball/def93f2a7841cfa1a4a1fa487b84054d0d53e521", + "reference": "def93f2a7841cfa1a4a1fa487b84054d0d53e521", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/messenger": "^5.3|^6.0" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "time": "2022-09-11T09:11:59+00:00", + "type": "symfony-messenger-bridge", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony AMQP extension Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/amqp-messenger/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/amqp-messenger" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", + "version_normalized": "2.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2022-01-02T09:53:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + }, + { + "name": "symfony/doctrine-messenger", + "version": "v5.4.12", + "version_normalized": "5.4.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-messenger.git", + "reference": "7649a80e917b47c5072480a2d763c2422da239d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/7649a80e917b47c5072480a2d763c2422da239d2", + "reference": "7649a80e917b47c5072480a2d763c2422da239d2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/messenger": "^5.1|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<2.13", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/dbal": "^2.13|^3.0", + "doctrine/persistence": "^1.3|^2|^3", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "time": "2022-08-09T12:54:00+00:00", + "type": "symfony-messenger-bridge", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Doctrine Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-messenger/tree/v5.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/doctrine-messenger" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.9", + "version_normalized": "5.4.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "time": "2022-05-05T16:45:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/event-dispatcher" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.2", + "version_normalized": "2.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "time": "2022-01-02T09:53:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/event-dispatcher-contracts" + }, + { + "name": "symfony/mailer", + "version": "v5.4.13", + "version_normalized": "5.4.13.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11", + "reference": "63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3", + "php": ">=7.2.5", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.2.6|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "require-dev": { + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/messenger": "^4.4|^5.0|^6.0" + }, + "time": "2022-08-29T06:47:07+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/mailer" + }, + { + "name": "symfony/messenger", + "version": "v5.4.13", + "version_normalized": "5.4.13.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "8f8d3425991e627902f8288088609a8d8f6c6ea4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/8f8d3425991e627902f8288088609a8d8f6c6ea4", + "reference": "8f8d3425991e627902f8288088609a8d8f6c6ea4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/log": "^1|^2|^3", + "symfony/amqp-messenger": "^5.1|^6.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/doctrine-messenger": "^5.1|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/redis-messenger": "^5.1|^6.0" + }, + "conflict": { + "symfony/event-dispatcher": "<4.4", + "symfony/framework-bundle": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/serializer": "<5.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/serializer": "^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/validator": "^4.4|^5.0|^6.0" + }, + "suggest": { + "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." + }, + "time": "2022-09-17T07:31:22+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/messenger" + }, + { + "name": "symfony/mime", + "version": "v5.4.13", + "version_normalized": "5.4.13.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd", + "reference": "bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.1|^6.0", + "symfony/property-info": "^4.4|^5.1|^6.0", + "symfony/serializer": "^5.2|^6.0" + }, + "time": "2022-09-01T18:18:29+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/mime" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", + "version_normalized": "1.26.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2022-05-24T11:49:31+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-idn" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "version_normalized": "1.26.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2022-05-24T11:49:31+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-normalizer" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.26.0", + "version_normalized": "1.26.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2022-05-10T07:21:04+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" + }, + { + "name": "symfony/redis-messenger", + "version": "v5.4.13", + "version_normalized": "5.4.13.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/redis-messenger.git", + "reference": "d3028b772de91e9aa0342c92ff71c77b130ac9c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/redis-messenger/zipball/d3028b772de91e9aa0342c92ff71c77b130ac9c4", + "reference": "d3028b772de91e9aa0342c92ff71c77b130ac9c4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/messenger": "^5.1|^6.0" + }, + "require-dev": { + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "time": "2022-09-11T09:11:59+00:00", + "type": "symfony-messenger-bridge", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Redis\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Redis extension Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/redis-messenger/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/redis-messenger" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.2", + "version_normalized": "2.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "time": "2022-05-30T19:17:29+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/service-contracts" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/plugins/email/vendor/composer/installed.php b/plugins/email/vendor/composer/installed.php new file mode 100644 index 0000000..b2fbb84 --- /dev/null +++ b/plugins/email/vendor/composer/installed.php @@ -0,0 +1,215 @@ + array( + 'name' => 'getgrav/grav-plugin-email', + 'pretty_version' => 'dev-develop', + 'version' => 'dev-develop', + 'reference' => 'f6eb15486887c489363e6bf79fa49ea4a3d833c6', + 'type' => 'grav-plugin', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'doctrine/lexer' => array( + 'pretty_version' => '1.2.3', + 'version' => '1.2.3.0', + 'reference' => 'c268e882d4dbdd85e36e4ad69e02dc284f89d229', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/lexer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'egulias/email-validator' => array( + 'pretty_version' => '3.2.1', + 'version' => '3.2.1.0', + 'reference' => 'f88dcf4b14af14a98ad96b14b2b317969eab6715', + 'type' => 'library', + 'install_path' => __DIR__ . '/../egulias/email-validator', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'getgrav/grav-plugin-email' => array( + 'pretty_version' => 'dev-develop', + 'version' => 'dev-develop', + 'reference' => 'f6eb15486887c489363e6bf79fa49ea4a3d833c6', + 'type' => 'grav-plugin', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/container' => array( + 'pretty_version' => '1.1.1', + 'version' => '1.1.1.0', + 'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/container', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/event-dispatcher' => array( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/event-dispatcher', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/event-dispatcher-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/log' => array( + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/amqp-messenger' => array( + 'pretty_version' => 'v5.4.13', + 'version' => '5.4.13.0', + 'reference' => 'def93f2a7841cfa1a4a1fa487b84054d0d53e521', + 'type' => 'symfony-messenger-bridge', + 'install_path' => __DIR__ . '/../symfony/amqp-messenger', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v2.5.2', + 'version' => '2.5.2.0', + 'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/doctrine-messenger' => array( + 'pretty_version' => 'v5.4.12', + 'version' => '5.4.12.0', + 'reference' => '7649a80e917b47c5072480a2d763c2422da239d2', + 'type' => 'symfony-messenger-bridge', + 'install_path' => __DIR__ . '/../symfony/doctrine-messenger', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher' => array( + 'pretty_version' => 'v5.4.9', + 'version' => '5.4.9.0', + 'reference' => '8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/event-dispatcher', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher-contracts' => array( + 'pretty_version' => 'v2.5.2', + 'version' => '2.5.2.0', + 'reference' => 'f98b54df6ad059855739db6fcbc2d36995283fe1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/event-dispatcher-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '2.0', + ), + ), + 'symfony/mailer' => array( + 'pretty_version' => 'v5.4.13', + 'version' => '5.4.13.0', + 'reference' => '63bf36a5150ac0bfed1c4d0a4e0b114a57b77e11', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/mailer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/messenger' => array( + 'pretty_version' => 'v5.4.13', + 'version' => '5.4.13.0', + 'reference' => '8f8d3425991e627902f8288088609a8d8f6c6ea4', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/messenger', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/mime' => array( + 'pretty_version' => 'v5.4.13', + 'version' => '5.4.13.0', + 'reference' => 'bb2ccf759e2b967dcd11bdee5bdf30dddd2290bd', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/mime', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-iconv' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-intl-idn' => array( + 'pretty_version' => 'v1.26.0', + 'version' => '1.26.0.0', + 'reference' => '59a8d271f00dd0e4c2e518104cc7963f655a1aa8', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-intl-normalizer' => array( + 'pretty_version' => 'v1.26.0', + 'version' => '1.26.0.0', + 'reference' => '219aa369ceff116e673852dce47c3a41794c14bd', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-mbstring' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php72' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php80' => array( + 'pretty_version' => 'v1.26.0', + 'version' => '1.26.0.0', + 'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/redis-messenger' => array( + 'pretty_version' => 'v5.4.13', + 'version' => '5.4.13.0', + 'reference' => 'd3028b772de91e9aa0342c92ff71c77b130ac9c4', + 'type' => 'symfony-messenger-bridge', + 'install_path' => __DIR__ . '/../symfony/redis-messenger', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/service-contracts' => array( + 'pretty_version' => 'v2.5.2', + 'version' => '2.5.2.0', + 'reference' => '4b426aac47d6427cc1a1d0f7e2ac724627f5966c', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/service-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/plugins/email/vendor/composer/platform_check.php b/plugins/email/vendor/composer/platform_check.php new file mode 100644 index 0000000..0109125 --- /dev/null +++ b/plugins/email/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70306)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.6". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/plugins/email/vendor/doctrine/lexer/LICENSE b/plugins/email/vendor/doctrine/lexer/LICENSE new file mode 100644 index 0000000..e8fdec4 --- /dev/null +++ b/plugins/email/vendor/doctrine/lexer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2006-2018 Doctrine Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/email/vendor/doctrine/lexer/README.md b/plugins/email/vendor/doctrine/lexer/README.md new file mode 100644 index 0000000..784f2a2 --- /dev/null +++ b/plugins/email/vendor/doctrine/lexer/README.md @@ -0,0 +1,9 @@ +# Doctrine Lexer + +[![Build Status](https://github.com/doctrine/lexer/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/lexer/actions) + +Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers. + +This lexer is used in Doctrine Annotations and in Doctrine ORM (DQL). + +https://www.doctrine-project.org/projects/lexer.html diff --git a/plugins/email/vendor/doctrine/lexer/composer.json b/plugins/email/vendor/doctrine/lexer/composer.json new file mode 100644 index 0000000..c435647 --- /dev/null +++ b/plugins/email/vendor/doctrine/lexer/composer.json @@ -0,0 +1,41 @@ +{ + "name": "doctrine/lexer", + "type": "library", + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "keywords": [ + "php", + "parser", + "lexer", + "annotations", + "docblock" + ], + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "license": "MIT", + "authors": [ + {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, + {"name": "Roman Borschel", "email": "roman@code-factory.org"}, + {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} + ], + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "autoload": { + "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" } + }, + "autoload-dev": { + "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine" } + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true + } +} diff --git a/plugins/email/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php b/plugins/email/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php new file mode 100644 index 0000000..7e8a11d --- /dev/null +++ b/plugins/email/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php @@ -0,0 +1,337 @@ + + */ + private $tokens = []; + + /** + * Current lexer position in input string. + * + * @var int + */ + private $position = 0; + + /** + * Current peek of current lexer position. + * + * @var int + */ + private $peek = 0; + + /** + * The next token in the input. + * + * @var mixed[]|null + * @psalm-var Token|null + */ + public $lookahead; + + /** + * The last matched/seen token. + * + * @var mixed[]|null + * @psalm-var Token|null + */ + public $token; + + /** + * Composed regex for input parsing. + * + * @var string|null + */ + private $regex; + + /** + * Sets the input data to be tokenized. + * + * The Lexer is immediately reset and the new input tokenized. + * Any unprocessed tokens from any previous input are lost. + * + * @param string $input The input to be tokenized. + * + * @return void + */ + public function setInput($input) + { + $this->input = $input; + $this->tokens = []; + + $this->reset(); + $this->scan($input); + } + + /** + * Resets the lexer. + * + * @return void + */ + public function reset() + { + $this->lookahead = null; + $this->token = null; + $this->peek = 0; + $this->position = 0; + } + + /** + * Resets the peek pointer to 0. + * + * @return void + */ + public function resetPeek() + { + $this->peek = 0; + } + + /** + * Resets the lexer position on the input to the given position. + * + * @param int $position Position to place the lexical scanner. + * + * @return void + */ + public function resetPosition($position = 0) + { + $this->position = $position; + } + + /** + * Retrieve the original lexer's input until a given position. + * + * @param int $position + * + * @return string + */ + public function getInputUntilPosition($position) + { + return substr($this->input, 0, $position); + } + + /** + * Checks whether a given token matches the current lookahead. + * + * @param int|string $type + * + * @return bool + */ + public function isNextToken($type) + { + return $this->lookahead !== null && $this->lookahead['type'] === $type; + } + + /** + * Checks whether any of the given tokens matches the current lookahead. + * + * @param list $types + * + * @return bool + */ + public function isNextTokenAny(array $types) + { + return $this->lookahead !== null && in_array($this->lookahead['type'], $types, true); + } + + /** + * Moves to the next token in the input string. + * + * @return bool + */ + public function moveNext() + { + $this->peek = 0; + $this->token = $this->lookahead; + $this->lookahead = isset($this->tokens[$this->position]) + ? $this->tokens[$this->position++] : null; + + return $this->lookahead !== null; + } + + /** + * Tells the lexer to skip input tokens until it sees a token with the given value. + * + * @param string $type The token type to skip until. + * + * @return void + */ + public function skipUntil($type) + { + while ($this->lookahead !== null && $this->lookahead['type'] !== $type) { + $this->moveNext(); + } + } + + /** + * Checks if given value is identical to the given token. + * + * @param mixed $value + * @param int|string $token + * + * @return bool + */ + public function isA($value, $token) + { + return $this->getType($value) === $token; + } + + /** + * Moves the lookahead token forward. + * + * @return mixed[]|null The next token or NULL if there are no more tokens ahead. + * @psalm-return Token|null + */ + public function peek() + { + if (isset($this->tokens[$this->position + $this->peek])) { + return $this->tokens[$this->position + $this->peek++]; + } + + return null; + } + + /** + * Peeks at the next token, returns it and immediately resets the peek. + * + * @return mixed[]|null The next token or NULL if there are no more tokens ahead. + * @psalm-return Token|null + */ + public function glimpse() + { + $peek = $this->peek(); + $this->peek = 0; + + return $peek; + } + + /** + * Scans the input string for tokens. + * + * @param string $input A query string. + * + * @return void + */ + protected function scan($input) + { + if (! isset($this->regex)) { + $this->regex = sprintf( + '/(%s)|%s/%s', + implode(')|(', $this->getCatchablePatterns()), + implode('|', $this->getNonCatchablePatterns()), + $this->getModifiers() + ); + } + + $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; + $matches = preg_split($this->regex, $input, -1, $flags); + + if ($matches === false) { + // Work around https://bugs.php.net/78122 + $matches = [[$input, 0]]; + } + + foreach ($matches as $match) { + // Must remain before 'value' assignment since it can change content + $type = $this->getType($match[0]); + + $this->tokens[] = [ + 'value' => $match[0], + 'type' => $type, + 'position' => $match[1], + ]; + } + } + + /** + * Gets the literal for a given token. + * + * @param int|string $token + * + * @return int|string + */ + public function getLiteral($token) + { + $className = static::class; + $reflClass = new ReflectionClass($className); + $constants = $reflClass->getConstants(); + + foreach ($constants as $name => $value) { + if ($value === $token) { + return $className . '::' . $name; + } + } + + return $token; + } + + /** + * Regex modifiers + * + * @return string + */ + protected function getModifiers() + { + return 'iu'; + } + + /** + * Lexical catchable patterns. + * + * @return string[] + */ + abstract protected function getCatchablePatterns(); + + /** + * Lexical non-catchable patterns. + * + * @return string[] + */ + abstract protected function getNonCatchablePatterns(); + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @param string $value + * + * @return int|string|null + */ + abstract protected function getType(&$value); +} diff --git a/plugins/email/vendor/doctrine/lexer/psalm.xml b/plugins/email/vendor/doctrine/lexer/psalm.xml new file mode 100644 index 0000000..f331e50 --- /dev/null +++ b/plugins/email/vendor/doctrine/lexer/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/plugins/email/vendor/egulias/email-validator/CHANGELOG.md b/plugins/email/vendor/egulias/email-validator/CHANGELOG.md new file mode 100644 index 0000000..83fd62e --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/CHANGELOG.md @@ -0,0 +1,33 @@ +# EmailValidator v3 Changelog + +## New Features + +* Access to local part and domain part from EmailParser +* Validations outside of the scope of the RFC will be considered "extra" validations, thus opening the door for adding new; will live in their own folder "extra" (as requested in #248, #195, #183). + +## Breacking changes + +* PHP version upgraded to match Symfony's (as of 12/2020). +* DNSCheckValidation now fails for missing MX records. While the RFC argues that the existence of only A records to be valid, starting in v3 they will be considered invalid. +* Emails domain part are now intenteded to be RFC 1035 compliant, rendering previous valid emails (e.g example@examp&) invalid. + +## PHP versions upgrade policy +PHP version upgrade requirement will happen via MINOR (3.x) version upgrades of the library, following the adoption level by major frameworks. + +## Changes +* #235 +* #215 +* #130 +* #258 +* #188 +* #181 +* #217 +* #214 +* #249 +* #236 +* #257 +* #210 + +## Thanks +To contributors, be it with PRs, reporting issues or supporting otherwise. + diff --git a/plugins/email/vendor/egulias/email-validator/CONTRIBUTING.md b/plugins/email/vendor/egulias/email-validator/CONTRIBUTING.md new file mode 100644 index 0000000..907bc2c --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/CONTRIBUTING.md @@ -0,0 +1,153 @@ +# Contributing + +When contributing to this repository make sure to follow the Pull request process below. +Reduce to the minimum 3rd party dependencies. + +Please note we have a [code of conduct](#Code of Conduct), please follow it in all your interactions with the project. + +## Pull Request Process + +When doing a PR to v2 remember that you also have to do the PR port to v3, or tests confirming the bug is not reproducible. + +1. Supported version is v3. If you are fixing a bug in v2, please port to v3 +2. Use the title as a brief description of the changes +3. Describe the changes you are proposing + 1. If adding an extra validation state the benefits of adding it and the problem is solving + 2. Document in the readme, by adding it to the list +4. Provide appropriate tests for the code you are submitting: aim to keep the existing coverage percentage. +5. Add your Twitter handle (if you have) so we can thank you there. + +## License +By contributing, you agree that your contributions will be licensed under its MIT License. + +## Code of Conduct + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at . +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +#### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/plugins/email/vendor/egulias/email-validator/LICENSE b/plugins/email/vendor/egulias/email-validator/LICENSE new file mode 100644 index 0000000..1f0f267 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-2021 Eduardo Gulias Davis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/egulias/email-validator/composer.json b/plugins/email/vendor/egulias/email-validator/composer.json new file mode 100644 index 0000000..d598d1b --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/composer.json @@ -0,0 +1,38 @@ +{ + "name": "egulias/email-validator", + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": ["email", "validation", "validator", "emailvalidation", "emailvalidator"], + "license": "MIT", + "authors": [ + {"name": "Eduardo Gulias Davis"} + ], + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "require": { + "php": ">=7.2", + "doctrine/lexer": "^1.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Egulias\\EmailValidator\\Tests\\": "tests" + } + } +} diff --git a/plugins/email/vendor/egulias/email-validator/composer.lock b/plugins/email/vendor/egulias/email-validator/composer.lock new file mode 100644 index 0000000..4b918d1 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/composer.lock @@ -0,0 +1,5028 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a77d36b64bc1213fecf4d4f92d759c3b", + "packages": [ + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "749045c69efb97c70d25d7463abba812e91f3a44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/749045c69efb97c70d25d7463abba812e91f3a44", + "reference": "749045c69efb97c70d25d7463abba812e91f3a44", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-14T14:02:44+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:17:38+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v2.6.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "psalm/phar": "^3.11@dev", + "react/promise": "^2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-02-20T17:52:18+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-03-30T17:13:30+00:00" + }, + { + "name": "composer/package-versions-deprecated", + "version": "1.11.99.5", + "source": { + "type": "git", + "url": "https://github.com/composer/package-versions-deprecated.git", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0", + "php": "^7 || ^8" + }, + "replace": { + "ocramius/package-versions": "1.11.99" + }, + "require-dev": { + "composer/composer": "^1.9.3 || ^2.0@dev", + "ext-zip": "^1.13", + "phpunit/phpunit": "^6.5 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-17T14:14:24+00:00" + }, + { + "name": "composer/pcre", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T20:21:48+00:00" + }, + { + "name": "composer/semver", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-04-01T19:23:25+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-03-03T08:28:38+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.2", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + }, + "time": "2022-03-02T22:36:06+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.4.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/74a8602c6faec9ef74b7a9391ac82c5e65b1cdab", + "reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.8.3 || ^2.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.4-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.4.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-05-25T13:24:33+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2", + "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.2.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-03-20T21:55:58+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0" + }, + "time": "2020-12-01T19:48:11+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.13.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + }, + "time": "2021-11-30T19:35:32+00:00" + }, + { + "name": "openlss/lib-array2xml", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/nullivex/lib-array2xml.git", + "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", + "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "LSS": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Bryan Tong", + "email": "bryan@nullivex.com", + "homepage": "https://www.nullivex.com" + }, + { + "name": "Tony Butler", + "email": "spudz76@gmail.com", + "homepage": "https://www.nullivex.com" + } + ], + "description": "Array2XML conversion library credit to lalit.org", + "homepage": "https://www.nullivex.com", + "keywords": [ + "array", + "array conversion", + "xml", + "xml conversion" + ], + "support": { + "issues": "https://github.com/nullivex/lib-array2xml/issues", + "source": "https://github.com/nullivex/lib-array2xml/tree/master" + }, + "time": "2019-03-29T20:06:56+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-coveralls/php-coveralls", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/php-coveralls/php-coveralls.git", + "reference": "007e13afdcdba2cd0efcc5f72c3b7efb356a8bd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/007e13afdcdba2cd0efcc5f72c3b7efb356a8bd4", + "reference": "007e13afdcdba2cd0efcc5f72c3b7efb356a8bd4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": "^5.5 || ^7.0 || ^8.0", + "psr/log": "^1.0 || ^2.0", + "symfony/config": "^2.1 || ^3.0 || ^4.0 || ^5.0 || ^6.0", + "symfony/console": "^2.1 || ^3.0 || ^4.0 || ^5.0 || ^6.0", + "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0", + "symfony/yaml": "^2.0.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "sanmai/phpunit-legacy-adapter": "^6.1 || ^8.0" + }, + "suggest": { + "symfony/http-kernel": "Allows Symfony integration" + }, + "bin": [ + "bin/php-coveralls" + ], + "type": "library", + "autoload": { + "psr-4": { + "PhpCoveralls\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp", + "role": "Original creator" + }, + { + "name": "Takashi Matsuo", + "email": "tmatsuo@google.com" + }, + { + "name": "Google Inc" + }, + { + "name": "Dariusz Ruminski", + "email": "dariusz.ruminski@gmail.com", + "homepage": "https://github.com/keradus" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-coveralls/php-coveralls/graphs/contributors" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/php-coveralls/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "support": { + "issues": "https://github.com/php-coveralls/php-coveralls/issues", + "source": "https://github.com/php-coveralls/php-coveralls/tree/v2.5.2" + }, + "time": "2021-12-06T17:05:08+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "77a32518733312af16a44300404e945338981de3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + }, + "time": "2022-03-15T21:29:03+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.2", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" + }, + "time": "2021-12-08T12:19:24+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.13.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-07T09:28:20+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", + "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.0", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-01T12:37:26+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-03T09:37:03+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-11-11T14:18:36+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-15T09:54:48+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/config", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "8f551fe22672ac7ab2c95fe46d899f960ed4d979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/8f551fe22672ac7ab2c95fe46d899f960ed4d979", + "reference": "8f551fe22672ac7ab2c95fe46d899f960ed4d979", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-17T10:39:36+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/829d5d1bf60b2efeb0887b7436873becc71a45eb", + "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-18T06:17:34+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/36a017fa4cce1eff1b8e8129ff53513abcef05ba", + "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-20T13:55:35+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-23T21:10:46+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:07:29+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v5.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/service-contracts": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v5.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-18T16:06:09+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", + "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-19T10:40:37+00:00" + }, + { + "name": "symfony/yaml", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "e80f87d2c9495966768310fc531b487ce64237a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e80f87d2c9495966768310fc531b487ce64237a2", + "reference": "e80f87d2c9495966768310fc531b487ce64237a2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.3" + }, + "require-dev": { + "symfony/console": "^5.3|^6.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-26T16:32:32+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "vimeo/psalm", + "version": "4.23.0", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "f1fe6ff483bf325c803df9f510d09a03fd796f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1fe6ff483bf325c803df9f510d09a03fd796f88", + "reference": "f1fe6ff483bf325c803df9f510d09a03fd796f88", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.4.2", + "amphp/byte-stream": "^1.5", + "composer/package-versions-deprecated": "^1.8.0", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.0.3", + "felixfbecker/language-server-protocol": "^1.5", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^4.13", + "openlss/lib-array2xml": "^1.0", + "php": "^7.1|^8", + "sebastian/diff": "^3.0 || ^4.0", + "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25", + "webmozart/path-util": "^2.3" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "brianium/paratest": "^4.0||^6.0", + "ext-curl": "*", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpdocumentor/reflection-docblock": "^5", + "phpmyadmin/sql-parser": "5.1.0||dev-master", + "phpspec/prophecy": ">=1.9.0", + "phpunit/phpunit": "^9.0", + "psalm/plugin-phpunit": "^0.16", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.3 || ^5.0 || ^6.0", + "weirdan/prophecy-shim": "^1.0 || ^2.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalter" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev", + "dev-3.x": "3.x-dev", + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php", + "src/spl_object_id.php" + ], + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php" + ], + "support": { + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/4.23.0" + }, + "time": "2022-04-28T17:35:49+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + }, + { + "name": "webmozart/path-util", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/path-util.git", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "webmozart/assert": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\PathUtil\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, + "abandoned": "symfony/filesystem", + "time": "2015-12-17T08:42:14+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.2" + }, + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/plugins/email/vendor/egulias/email-validator/src/EmailLexer.php b/plugins/email/vendor/egulias/email-validator/src/EmailLexer.php new file mode 100644 index 0000000..7e660b6 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/EmailLexer.php @@ -0,0 +1,339 @@ + self::S_OPENCURLYBRACES, + '}' => self::S_CLOSECURLYBRACES, + '(' => self::S_OPENPARENTHESIS, + ')' => self::S_CLOSEPARENTHESIS, + '<' => self::S_LOWERTHAN, + '>' => self::S_GREATERTHAN, + '[' => self::S_OPENBRACKET, + ']' => self::S_CLOSEBRACKET, + ':' => self::S_COLON, + ';' => self::S_SEMICOLON, + '@' => self::S_AT, + '\\' => self::S_BACKSLASH, + '/' => self::S_SLASH, + ',' => self::S_COMMA, + '.' => self::S_DOT, + "'" => self::S_SQUOTE, + "`" => self::S_BACKTICK, + '"' => self::S_DQUOTE, + '-' => self::S_HYPHEN, + '::' => self::S_DOUBLECOLON, + ' ' => self::S_SP, + "\t" => self::S_HTAB, + "\r" => self::S_CR, + "\n" => self::S_LF, + "\r\n" => self::CRLF, + 'IPv6' => self::S_IPV6TAG, + '' => self::S_EMPTY, + '\0' => self::C_NUL, + '*' => self::ASTERISK, + '!' => self::EXCLAMATION, + '&' => self::AMPERSAND, + '^' => self::CARET, + '$' => self::DOLLAR, + '%' => self::PERCENTAGE, + '~' => self::S_TILDE, + '|' => self::S_PIPE, + '_' => self::S_UNDERSCORE, + '=' => self::S_EQUAL, + '+' => self::S_PLUS, + '¿' => self::INVERT_QUESTIONMARK, + '?' => self::QUESTIONMARK, + '#' => self::NUMBER_SIGN, + '¡' => self::INVERT_EXCLAMATION, + ]; + + const INVALID_CHARS_REGEX = "/[^\p{S}\p{C}\p{Cc}]+/iu"; + + const VALID_UTF8_REGEX = '/\p{Cc}+/u'; + + const CATCHABLE_PATTERNS = [ + '[a-zA-Z]+[46]?', //ASCII and domain literal + '[^\x00-\x7F]', //UTF-8 + '[0-9]+', + '\r\n', + '::', + '\s+?', + '.', + ]; + + const NON_CATCHABLE_PATTERNS = [ + '[\xA0-\xff]+', + ]; + + const MODIFIERS = 'iu'; + + /** @var bool */ + protected $hasInvalidTokens = false; + + /** + * @var array + * + * @psalm-var array{value:string, type:null|int, position:int}|array + */ + protected $previous = []; + + /** + * The last matched/seen token. + * + * @var array + * + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-var array{value:string, type:null|int, position:int} + * @psalm-suppress NonInvariantDocblockPropertyType + */ + public $token; + + /** + * The next token in the input. + * + * @var array{position: int, type: int|null|string, value: int|string}|null + */ + public $lookahead; + + /** @psalm-var array{value:'', type:null, position:0} */ + private static $nullToken = [ + 'value' => '', + 'type' => null, + 'position' => 0, + ]; + + /** @var string */ + private $accumulator = ''; + + /** @var bool */ + private $hasToRecord = false; + + public function __construct() + { + $this->previous = $this->token = self::$nullToken; + $this->lookahead = null; + } + + public function reset() : void + { + $this->hasInvalidTokens = false; + parent::reset(); + $this->previous = $this->token = self::$nullToken; + } + + /** + * @param int $type + * @throws \UnexpectedValueException + * @return boolean + * + * @psalm-suppress InvalidScalarArgument + */ + public function find($type) : bool + { + $search = clone $this; + $search->skipUntil($type); + + if (!$search->lookahead) { + throw new \UnexpectedValueException($type . ' not found'); + } + return true; + } + + /** + * moveNext + * + * @return boolean + */ + public function moveNext() : bool + { + if ($this->hasToRecord && $this->previous === self::$nullToken) { + $this->accumulator .= $this->token['value']; + } + + $this->previous = $this->token; + + if($this->lookahead === null) { + $this->lookahead = self::$nullToken; + } + + $hasNext = parent::moveNext(); + + if ($this->hasToRecord) { + $this->accumulator .= $this->token['value']; + } + + return $hasNext; + } + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @param string $value + * @throws \InvalidArgumentException + * @return integer + */ + protected function getType(&$value) + { + $encoded = $value; + + if (mb_detect_encoding($value, 'auto', true) !== 'UTF-8') { + $encoded = mb_convert_encoding($value, 'UTF-8', 'Windows-1252'); + } + + if ($this->isValid($encoded)) { + return $this->charValue[$encoded]; + } + + if ($this->isNullType($encoded)) { + return self::C_NUL; + } + + if ($this->isInvalidChar($encoded)) { + $this->hasInvalidTokens = true; + return self::INVALID; + } + + + return self::GENERIC; + } + + protected function isValid(string $value) : bool + { + return isset($this->charValue[$value]); + } + + protected function isNullType(string $value) : bool + { + return $value === "\0"; + } + + protected function isInvalidChar(string $value) : bool + { + return !preg_match(self::INVALID_CHARS_REGEX, $value); + } + + protected function isUTF8Invalid(string $value) : bool + { + return preg_match(self::VALID_UTF8_REGEX, $value) !== false; + } + + public function hasInvalidTokens() : bool + { + return $this->hasInvalidTokens; + } + + /** + * getPrevious + * + * @return array + */ + public function getPrevious() : array + { + return $this->previous; + } + + /** + * Lexical catchable patterns. + * + * @return string[] + */ + protected function getCatchablePatterns() : array + { + return self::CATCHABLE_PATTERNS; + } + + /** + * Lexical non-catchable patterns. + * + * @return string[] + */ + protected function getNonCatchablePatterns() : array + { + return self::NON_CATCHABLE_PATTERNS; + } + + protected function getModifiers() : string + { + return self::MODIFIERS; + } + + public function getAccumulatedValues() : string + { + return $this->accumulator; + } + + public function startRecording() : void + { + $this->hasToRecord = true; + } + + public function stopRecording() : void + { + $this->hasToRecord = false; + } + + public function clearRecorded() : void + { + $this->accumulator = ''; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/EmailParser.php b/plugins/email/vendor/egulias/email-validator/src/EmailParser.php new file mode 100644 index 0000000..c78f74a --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/EmailParser.php @@ -0,0 +1,91 @@ +addLongEmailWarning($this->localPart, $this->domainPart); + + return $result; + } + + protected function preLeftParsing(): Result + { + if (!$this->hasAtToken()) { + return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]); + } + return new ValidEmail(); + } + + protected function parseLeftFromAt(): Result + { + return $this->processLocalPart(); + } + + protected function parseRightFromAt(): Result + { + return $this->processDomainPart(); + } + + private function processLocalPart() : Result + { + $localPartParser = new LocalPart($this->lexer); + $localPartResult = $localPartParser->parse(); + $this->localPart = $localPartParser->localPart(); + $this->warnings = array_merge($localPartParser->getWarnings(), $this->warnings); + + return $localPartResult; + } + + private function processDomainPart() : Result + { + $domainPartParser = new DomainPart($this->lexer); + $domainPartResult = $domainPartParser->parse(); + $this->domainPart = $domainPartParser->domainPart(); + $this->warnings = array_merge($domainPartParser->getWarnings(), $this->warnings); + + return $domainPartResult; + } + + public function getDomainPart() : string + { + return $this->domainPart; + } + + public function getLocalPart() : string + { + return $this->localPart; + } + + private function addLongEmailWarning(string $localPart, string $parsedDomainPart) : void + { + if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAIL_MAX_LENGTH) { + $this->warnings[EmailTooLong::CODE] = new EmailTooLong(); + } + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/EmailValidator.php b/plugins/email/vendor/egulias/email-validator/src/EmailValidator.php new file mode 100644 index 0000000..5a2e5c8 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/EmailValidator.php @@ -0,0 +1,67 @@ +lexer = new EmailLexer(); + } + + /** + * @param string $email + * @param EmailValidation $emailValidation + * @return bool + */ + public function isValid(string $email, EmailValidation $emailValidation) + { + $isValid = $emailValidation->isValid($email, $this->lexer); + $this->warnings = $emailValidation->getWarnings(); + $this->error = $emailValidation->getError(); + + return $isValid; + } + + /** + * @return boolean + */ + public function hasWarnings() + { + return !empty($this->warnings); + } + + /** + * @return array + */ + public function getWarnings() + { + return $this->warnings; + } + + /** + * @return InvalidEmail|null + */ + public function getError() + { + return $this->error; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/MessageIDParser.php b/plugins/email/vendor/egulias/email-validator/src/MessageIDParser.php new file mode 100644 index 0000000..fe58b9f --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/MessageIDParser.php @@ -0,0 +1,92 @@ +addLongEmailWarning($this->idLeft, $this->idRight); + + return $result; + } + + protected function preLeftParsing(): Result + { + if (!$this->hasAtToken()) { + return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]); + } + return new ValidEmail(); + } + + protected function parseLeftFromAt(): Result + { + return $this->processIDLeft(); + } + + protected function parseRightFromAt(): Result + { + return $this->processIDRight(); + } + + private function processIDLeft() : Result + { + $localPartParser = new IDLeftPart($this->lexer); + $localPartResult = $localPartParser->parse(); + $this->idLeft = $localPartParser->localPart(); + $this->warnings = array_merge($localPartParser->getWarnings(), $this->warnings); + + return $localPartResult; + } + + private function processIDRight() : Result + { + $domainPartParser = new IDRightPart($this->lexer); + $domainPartResult = $domainPartParser->parse(); + $this->idRight = $domainPartParser->domainPart(); + $this->warnings = array_merge($domainPartParser->getWarnings(), $this->warnings); + + return $domainPartResult; + } + + public function getLeftPart() : string + { + return $this->idLeft; + } + + public function getRightPart() : string + { + return $this->idRight; + } + + private function addLongEmailWarning(string $localPart, string $parsedDomainPart) : void + { + if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAILID_MAX_LENGTH) { + $this->warnings[EmailTooLong::CODE] = new EmailTooLong(); + } + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser.php b/plugins/email/vendor/egulias/email-validator/src/Parser.php new file mode 100644 index 0000000..b1905f9 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser.php @@ -0,0 +1,78 @@ +lexer = $lexer; + } + + public function parse(string $str) : Result + { + $this->lexer->setInput($str); + + if ($this->lexer->hasInvalidTokens()) { + return new InvalidEmail(new ExpectingATEXT("Invalid tokens found"), $this->lexer->token["value"]); + } + + $preParsingResult = $this->preLeftParsing(); + if ($preParsingResult->isInvalid()) { + return $preParsingResult; + } + + $localPartResult = $this->parseLeftFromAt(); + + if ($localPartResult->isInvalid()) { + return $localPartResult; + } + + $domainPartResult = $this->parseRightFromAt(); + + if ($domainPartResult->isInvalid()) { + return $domainPartResult; + } + + return new ValidEmail(); + } + + /** + * @return Warning\Warning[] + */ + public function getWarnings() : array + { + return $this->warnings; + } + + protected function hasAtToken() : bool + { + $this->lexer->moveNext(); + $this->lexer->moveNext(); + + return $this->lexer->token['type'] !== EmailLexer::S_AT; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/Comment.php b/plugins/email/vendor/egulias/email-validator/src/Parser/Comment.php new file mode 100644 index 0000000..ffa6128 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/Comment.php @@ -0,0 +1,103 @@ +lexer = $lexer; + $this->commentStrategy = $commentStrategy; + } + + public function parse() : Result + { + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) { + $this->openedParenthesis++; + if($this->noClosingParenthesis()) { + return new InvalidEmail(new UnclosedComment(), $this->lexer->token['value']); + } + } + + if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) { + return new InvalidEmail(new UnOpenedComment(), $this->lexer->token['value']); + } + + $this->warnings[WarningComment::CODE] = new WarningComment(); + + $moreTokens = true; + while ($this->commentStrategy->exitCondition($this->lexer, $this->openedParenthesis) && $moreTokens){ + + if ($this->lexer->isNextToken(EmailLexer::S_OPENPARENTHESIS)) { + $this->openedParenthesis++; + } + $this->warnEscaping(); + if($this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) { + $this->openedParenthesis--; + } + $moreTokens = $this->lexer->moveNext(); + } + + if($this->openedParenthesis >= 1) { + return new InvalidEmail(new UnclosedComment(), $this->lexer->token['value']); + } else if ($this->openedParenthesis < 0) { + return new InvalidEmail(new UnOpenedComment(), $this->lexer->token['value']); + } + + $finalValidations = $this->commentStrategy->endOfLoopValidations($this->lexer); + + $this->warnings = array_merge($this->warnings, $this->commentStrategy->getWarnings()); + + return $finalValidations; + } + + + /** + * @return bool + */ + private function warnEscaping() : bool + { + //Backslash found + if ($this->lexer->token['type'] !== EmailLexer::S_BACKSLASH) { + return false; + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) { + return false; + } + + $this->warnings[QuotedPart::CODE] = + new QuotedPart($this->lexer->getPrevious()['type'], $this->lexer->token['type']); + return true; + + } + + private function noClosingParenthesis() : bool + { + try { + $this->lexer->find(EmailLexer::S_CLOSEPARENTHESIS); + return false; + } catch (\RuntimeException $e) { + return true; + } + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php b/plugins/email/vendor/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php new file mode 100644 index 0000000..c388efd --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php @@ -0,0 +1,18 @@ +isNextToken(EmailLexer::S_DOT))){ // || !$internalLexer->moveNext()) { + return false; + } + + return true; + } + + public function endOfLoopValidations(EmailLexer $lexer) : Result + { + //test for end of string + if (!$lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new ExpectingATEXT('DOT not found near CLOSEPARENTHESIS'), $lexer->token['value']); + } + //add warning + //Address is valid within the message but cannot be used unmodified for the envelope + return new ValidEmail(); + } + + public function getWarnings(): array + { + return []; + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php b/plugins/email/vendor/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php new file mode 100644 index 0000000..73bc7b2 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php @@ -0,0 +1,37 @@ +isNextToken(EmailLexer::S_AT); + } + + public function endOfLoopValidations(EmailLexer $lexer) : Result + { + if (!$lexer->isNextToken(EmailLexer::S_AT)) { + return new InvalidEmail(new ExpectingATEXT('ATEX is not expected after closing comments'), $lexer->token['value']); + } + $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt(); + return new ValidEmail(); + } + + public function getWarnings(): array + { + return $this->warnings; + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/DomainLiteral.php b/plugins/email/vendor/egulias/email-validator/src/Parser/DomainLiteral.php new file mode 100644 index 0000000..4e5b8bf --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/DomainLiteral.php @@ -0,0 +1,211 @@ +addTagWarnings(); + + $IPv6TAG = false; + $addressLiteral = ''; + + do { + if ($this->lexer->token['type'] === EmailLexer::C_NUL) { + return new InvalidEmail(new ExpectingDTEXT(), $this->lexer->token['value']); + } + + $this->addObsoleteWarnings(); + + if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENBRACKET, EmailLexer::S_OPENBRACKET))) { + return new InvalidEmail(new ExpectingDTEXT(), $this->lexer->token['value']); + } + + if ($this->lexer->isNextTokenAny( + array(EmailLexer::S_HTAB, EmailLexer::S_SP, EmailLexer::CRLF) + )) { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + $this->parseFWS(); + } + + if ($this->lexer->isNextToken(EmailLexer::S_CR)) { + return new InvalidEmail(new CRNoLF(), $this->lexer->token['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) { + return new InvalidEmail(new UnusualElements($this->lexer->token['value']), $this->lexer->token['value']); + } + if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) { + $IPv6TAG = true; + } + + if ($this->lexer->token['type'] === EmailLexer::S_CLOSEBRACKET) { + break; + } + + $addressLiteral .= $this->lexer->token['value']; + + } while ($this->lexer->moveNext()); + + + //Encapsulate + $addressLiteral = str_replace('[', '', $addressLiteral); + $isAddressLiteralIPv4 = $this->checkIPV4Tag($addressLiteral); + + if (!$isAddressLiteralIPv4) { + return new ValidEmail(); + } else { + $addressLiteral = $this->convertIPv4ToIPv6($addressLiteral); + } + + if (!$IPv6TAG) { + $this->warnings[WarningDomainLiteral::CODE] = new WarningDomainLiteral(); + return new ValidEmail(); + } + + $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); + + $this->checkIPV6Tag($addressLiteral); + + return new ValidEmail(); + } + + /** + * @param string $addressLiteral + * @param int $maxGroups + */ + public function checkIPV6Tag($addressLiteral, $maxGroups = 8) : void + { + $prev = $this->lexer->getPrevious(); + if ($prev['type'] === EmailLexer::S_COLON) { + $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd(); + } + + $IPv6 = substr($addressLiteral, 5); + //Daniel Marschall's new IPv6 testing strategy + $matchesIP = explode(':', $IPv6); + $groupCount = count($matchesIP); + $colons = strpos($IPv6, '::'); + + if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) { + $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar(); + } + + if ($colons === false) { + // We need exactly the right number of groups + if ($groupCount !== $maxGroups) { + $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount(); + } + return; + } + + if ($colons !== strrpos($IPv6, '::')) { + $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon(); + return; + } + + if ($colons === 0 || $colons === (strlen($IPv6) - 2)) { + // RFC 4291 allows :: at the start or end of an address + //with 7 other groups in addition + ++$maxGroups; + } + + if ($groupCount > $maxGroups) { + $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups(); + } elseif ($groupCount === $maxGroups) { + $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated(); + } + } + + public function convertIPv4ToIPv6(string $addressLiteralIPv4) : string + { + $matchesIP = []; + $IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteralIPv4, $matchesIP); + + // Extract IPv4 part from the end of the address-literal (if there is one) + if ($IPv4Match > 0) { + $index = (int) strrpos($addressLiteralIPv4, $matchesIP[0]); + //There's a match but it is at the start + if ($index > 0) { + // Convert IPv4 part to IPv6 format for further testing + return substr($addressLiteralIPv4, 0, $index) . '0:0'; + } + } + + return $addressLiteralIPv4; + } + + /** + * @param string $addressLiteral + * + * @return bool + */ + protected function checkIPV4Tag($addressLiteral) : bool + { + $matchesIP = []; + $IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteral, $matchesIP); + + // Extract IPv4 part from the end of the address-literal (if there is one) + + if ($IPv4Match > 0) { + $index = strrpos($addressLiteral, $matchesIP[0]); + //There's a match but it is at the start + if ($index === 0) { + $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); + return false; + } + } + + return true; + } + + private function addObsoleteWarnings() : void + { + if(in_array($this->lexer->token['type'], self::OBSOLETE_WARNINGS)) { + $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT(); + } + } + + private function addTagWarnings() : void + { + if ($this->lexer->isNextToken(EmailLexer::S_COLON)) { + $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); + } + if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) { + $lexer = clone $this->lexer; + $lexer->moveNext(); + if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) { + $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); + } + } + } + +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/DomainPart.php b/plugins/email/vendor/egulias/email-validator/src/Parser/DomainPart.php new file mode 100644 index 0000000..f171740 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/DomainPart.php @@ -0,0 +1,312 @@ +lexer->clearRecorded(); + $this->lexer->startRecording(); + + $this->lexer->moveNext(); + + $domainChecks = $this->performDomainStartChecks(); + if ($domainChecks->isInvalid()) { + return $domainChecks; + } + + if ($this->lexer->token['type'] === EmailLexer::S_AT) { + return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']); + } + + $result = $this->doParseDomainPart(); + if ($result->isInvalid()) { + return $result; + } + + $end = $this->checkEndOfDomain(); + if ($end->isInvalid()) { + return $end; + } + + $this->lexer->stopRecording(); + $this->domainPart = $this->lexer->getAccumulatedValues(); + + $length = strlen($this->domainPart); + if ($length > self::DOMAIN_MAX_LENGTH) { + return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']); + } + + return new ValidEmail(); + } + + private function checkEndOfDomain() : Result + { + $prev = $this->lexer->getPrevious(); + if ($prev['type'] === EmailLexer::S_DOT) { + return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']); + } + if ($prev['type'] === EmailLexer::S_HYPHEN) { + return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_SP) { + return new InvalidEmail(new CRLFAtTheEnd(), $prev['value']); + } + return new ValidEmail(); + + } + + private function performDomainStartChecks() : Result + { + $invalidTokens = $this->checkInvalidTokensAfterAT(); + if ($invalidTokens->isInvalid()) { + return $invalidTokens; + } + + $missingDomain = $this->checkEmptyDomain(); + if ($missingDomain->isInvalid()) { + return $missingDomain; + } + + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) { + $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment(); + } + return new ValidEmail(); + } + + private function checkEmptyDomain() : Result + { + $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY || + ($this->lexer->token['type'] === EmailLexer::S_SP && + !$this->lexer->isNextToken(EmailLexer::GENERIC)); + + if ($thereIsNoDomain) { + return new InvalidEmail(new NoDomainPart(), $this->lexer->token['value']); + } + + return new ValidEmail(); + } + + private function checkInvalidTokensAfterAT() : Result + { + if ($this->lexer->token['type'] === EmailLexer::S_DOT) { + return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']); + } + if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) { + return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->token['value']); + } + return new ValidEmail(); + } + + protected function parseComments(): Result + { + $commentParser = new Comment($this->lexer, new DomainComment()); + $result = $commentParser->parse(); + $this->warnings = array_merge($this->warnings, $commentParser->getWarnings()); + + return $result; + } + + protected function doParseDomainPart() : Result + { + $tldMissing = true; + $hasComments = false; + $domain = ''; + do { + $prev = $this->lexer->getPrevious(); + + $notAllowedChars = $this->checkNotAllowedChars($this->lexer->token); + if ($notAllowedChars->isInvalid()) { + return $notAllowedChars; + } + + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS || + $this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS ) { + $hasComments = true; + $commentsResult = $this->parseComments(); + + //Invalid comment parsing + if($commentsResult->isInvalid()) { + return $commentsResult; + } + } + + $dotsResult = $this->checkConsecutiveDots(); + if ($dotsResult->isInvalid()) { + return $dotsResult; + } + + if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET) { + $literalResult = $this->parseDomainLiteral(); + + $this->addTLDWarnings($tldMissing); + return $literalResult; + } + + $labelCheck = $this->checkLabelLength(); + if ($labelCheck->isInvalid()) { + return $labelCheck; + } + + $FwsResult = $this->parseFWS(); + if($FwsResult->isInvalid()) { + return $FwsResult; + } + + $domain .= $this->lexer->token['value']; + + if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::GENERIC)) { + $tldMissing = false; + } + + $exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments); + if ($exceptionsResult->isInvalid()) { + return $exceptionsResult; + } + $this->lexer->moveNext(); + + } while (null !== $this->lexer->token['type']); + + $labelCheck = $this->checkLabelLength(true); + if ($labelCheck->isInvalid()) { + return $labelCheck; + } + $this->addTLDWarnings($tldMissing); + + $this->domainPart = $domain; + return new ValidEmail(); + } + + private function checkNotAllowedChars(array $token) : Result + { + $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true]; + if (isset($notAllowed[$token['type']])) { + return new InvalidEmail(new CharNotAllowed(), $token['value']); + } + return new ValidEmail(); + } + + /** + * @return Result + */ + protected function parseDomainLiteral() : Result + { + try { + $this->lexer->find(EmailLexer::S_CLOSEBRACKET); + } catch (\RuntimeException $e) { + return new InvalidEmail(new ExpectingDomainLiteralClose(), $this->lexer->token['value']); + } + + $domainLiteralParser = new DomainLiteralParser($this->lexer); + $result = $domainLiteralParser->parse(); + $this->warnings = array_merge($this->warnings, $domainLiteralParser->getWarnings()); + return $result; + } + + protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result + { + if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) { + return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->token['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->token['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH + && $this->lexer->isNextToken(EmailLexer::GENERIC)) { + return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']); + } + + return $this->validateTokens($hasComments); + } + + protected function validateTokens(bool $hasComments) : Result + { + $validDomainTokens = array( + EmailLexer::GENERIC => true, + EmailLexer::S_HYPHEN => true, + EmailLexer::S_DOT => true, + ); + + if ($hasComments) { + $validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true; + $validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true; + } + + if (!isset($validDomainTokens[$this->lexer->token['type']])) { + return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']); + } + + return new ValidEmail(); + } + + private function checkLabelLength(bool $isEndOfDomain = false) : Result + { + if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) { + if ($this->isLabelTooLong($this->label)) { + return new InvalidEmail(new LabelTooLong(), $this->lexer->token['value']); + } + $this->label = ''; + } + $this->label .= $this->lexer->token['value']; + return new ValidEmail(); + } + + + private function isLabelTooLong(string $label) : bool + { + if (preg_match('/[^\x00-\x7F]/', $label)) { + idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo); + return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG); + } + return strlen($label) > self::LABEL_MAX_LENGTH; + } + + private function addTLDWarnings(bool $isTLDMissing) : void + { + if ($isTLDMissing) { + $this->warnings[TLD::CODE] = new TLD(); + } + } + + public function domainPart() : string + { + return $this->domainPart; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/DoubleQuote.php b/plugins/email/vendor/egulias/email-validator/src/Parser/DoubleQuote.php new file mode 100644 index 0000000..8b83273 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/DoubleQuote.php @@ -0,0 +1,88 @@ +checkDQUOTE(); + if($validQuotedString->isInvalid()) return $validQuotedString; + + $special = [ + EmailLexer::S_CR => true, + EmailLexer::S_HTAB => true, + EmailLexer::S_LF => true + ]; + + $invalid = [ + EmailLexer::C_NUL => true, + EmailLexer::S_HTAB => true, + EmailLexer::S_CR => true, + EmailLexer::S_LF => true + ]; + + $setSpecialsWarning = true; + + $this->lexer->moveNext(); + + while ($this->lexer->token['type'] !== EmailLexer::S_DQUOTE && null !== $this->lexer->token['type']) { + if (isset($special[$this->lexer->token['type']]) && $setSpecialsWarning) { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + $setSpecialsWarning = false; + } + if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH && $this->lexer->isNextToken(EmailLexer::S_DQUOTE)) { + $this->lexer->moveNext(); + } + + $this->lexer->moveNext(); + + if (!$this->escaped() && isset($invalid[$this->lexer->token['type']])) { + return new InvalidEmail(new ExpectingATEXT("Expecting ATEXT between DQUOTE"), $this->lexer->token['value']); + } + } + + $prev = $this->lexer->getPrevious(); + + if ($prev['type'] === EmailLexer::S_BACKSLASH) { + $validQuotedString = $this->checkDQUOTE(); + if($validQuotedString->isInvalid()) return $validQuotedString; + } + + if (!$this->lexer->isNextToken(EmailLexer::S_AT) && $prev['type'] !== EmailLexer::S_BACKSLASH) { + return new InvalidEmail(new ExpectingATEXT("Expecting ATEXT between DQUOTE"), $this->lexer->token['value']); + } + + return new ValidEmail(); + } + + protected function checkDQUOTE() : Result + { + $previous = $this->lexer->getPrevious(); + + if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] === EmailLexer::GENERIC) { + $description = 'https://tools.ietf.org/html/rfc5322#section-3.2.4 - quoted string should be a unit'; + return new InvalidEmail(new ExpectingATEXT($description), $this->lexer->token['value']); + } + + try { + $this->lexer->find(EmailLexer::S_DQUOTE); + } catch (\Exception $e) { + return new InvalidEmail(new UnclosedQuotedString(), $this->lexer->token['value']); + } + $this->warnings[QuotedString::CODE] = new QuotedString($previous['value'], $this->lexer->token['value']); + + return new ValidEmail(); + } + +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/FoldingWhiteSpace.php b/plugins/email/vendor/egulias/email-validator/src/Parser/FoldingWhiteSpace.php new file mode 100644 index 0000000..da17d43 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/FoldingWhiteSpace.php @@ -0,0 +1,86 @@ +isFWS()) { + return new ValidEmail(); + } + + $previous = $this->lexer->getPrevious(); + + $resultCRLF = $this->checkCRLFInFWS(); + if ($resultCRLF->isInvalid()) { + return $resultCRLF; + } + + if ($this->lexer->token['type'] === EmailLexer::S_CR) { + return new InvalidEmail(new CRNoLF(), $this->lexer->token['value']); + } + + if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] !== EmailLexer::S_AT) { + return new InvalidEmail(new AtextAfterCFWS(), $this->lexer->token['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_LF || $this->lexer->token['type'] === EmailLexer::C_NUL) { + return new InvalidEmail(new ExpectingCTEXT(), $this->lexer->token['value']); + } + + if ($this->lexer->isNextToken(EmailLexer::S_AT) || $previous['type'] === EmailLexer::S_AT) { + $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt(); + } else { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + } + + return new ValidEmail(); + } + + protected function checkCRLFInFWS() : Result + { + if ($this->lexer->token['type'] !== EmailLexer::CRLF) { + return new ValidEmail(); + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) { + return new InvalidEmail(new CRLFX2(), $this->lexer->token['value']); + } + + //this has no coverage. Condition is repeated from above one + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) { + return new InvalidEmail(new CRLFAtTheEnd(), $this->lexer->token['value']); + } + + return new ValidEmail(); + } + + protected function isFWS() : bool + { + if ($this->escaped()) { + return false; + } + + return in_array($this->lexer->token['type'], self::FWS_TYPES); + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/IDLeftPart.php b/plugins/email/vendor/egulias/email-validator/src/Parser/IDLeftPart.php new file mode 100644 index 0000000..abb4982 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/IDLeftPart.php @@ -0,0 +1,16 @@ +lexer->token['value']); + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/IDRightPart.php b/plugins/email/vendor/egulias/email-validator/src/Parser/IDRightPart.php new file mode 100644 index 0000000..2432562 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/IDRightPart.php @@ -0,0 +1,29 @@ + true, + EmailLexer::S_SQUOTE => true, + EmailLexer::S_BACKTICK => true, + EmailLexer::S_SEMICOLON => true, + EmailLexer::S_GREATERTHAN => true, + EmailLexer::S_LOWERTHAN => true, + ]; + + if (isset($invalidDomainTokens[$this->lexer->token['type']])) { + return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']); + } + return new ValidEmail(); + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/LocalPart.php b/plugins/email/vendor/egulias/email-validator/src/Parser/LocalPart.php new file mode 100644 index 0000000..90f01a5 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/LocalPart.php @@ -0,0 +1,165 @@ + EmailLexer::S_COMMA, + EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET, + EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET, + EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN, + EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN, + EmailLexer::S_COLON => EmailLexer::S_COLON, + EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON, + EmailLexer::INVALID => EmailLexer::INVALID + ]; + + /** + * @var string + */ + private $localPart = ''; + + + public function parse() : Result + { + $this->lexer->startRecording(); + + while ($this->lexer->token['type'] !== EmailLexer::S_AT && null !== $this->lexer->token['type']) { + if ($this->hasDotAtStart()) { + return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_DQUOTE) { + $dquoteParsingResult = $this->parseDoubleQuote(); + + //Invalid double quote parsing + if($dquoteParsingResult->isInvalid()) { + return $dquoteParsingResult; + } + } + + if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS || + $this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS ) { + $commentsResult = $this->parseComments(); + + //Invalid comment parsing + if($commentsResult->isInvalid()) { + return $commentsResult; + } + } + + if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new ConsecutiveDot(), $this->lexer->token['value']); + } + + if ($this->lexer->token['type'] === EmailLexer::S_DOT && + $this->lexer->isNextToken(EmailLexer::S_AT) + ) { + return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']); + } + + $resultEscaping = $this->validateEscaping(); + if ($resultEscaping->isInvalid()) { + return $resultEscaping; + } + + $resultToken = $this->validateTokens(false); + if ($resultToken->isInvalid()) { + return $resultToken; + } + + $resultFWS = $this->parseLocalFWS(); + if($resultFWS->isInvalid()) { + return $resultFWS; + } + + $this->lexer->moveNext(); + } + + $this->lexer->stopRecording(); + $this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@'); + if (strlen($this->localPart) > LocalTooLong::LOCAL_PART_LENGTH) { + $this->warnings[LocalTooLong::CODE] = new LocalTooLong(); + } + + return new ValidEmail(); + } + + protected function validateTokens(bool $hasComments) : Result + { + if (isset(self::INVALID_TOKENS[$this->lexer->token['type']])) { + return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']); + } + return new ValidEmail(); + } + + public function localPart() : string + { + return $this->localPart; + } + + private function parseLocalFWS() : Result + { + $foldingWS = new FoldingWhiteSpace($this->lexer); + $resultFWS = $foldingWS->parse(); + if ($resultFWS->isValid()) { + $this->warnings = array_merge($this->warnings, $foldingWS->getWarnings()); + } + return $resultFWS; + } + + private function hasDotAtStart() : bool + { + return $this->lexer->token['type'] === EmailLexer::S_DOT && null === $this->lexer->getPrevious()['type']; + } + + private function parseDoubleQuote() : Result + { + $dquoteParser = new DoubleQuote($this->lexer); + $parseAgain = $dquoteParser->parse(); + $this->warnings = array_merge($this->warnings, $dquoteParser->getWarnings()); + + return $parseAgain; + } + + protected function parseComments(): Result + { + $commentParser = new Comment($this->lexer, new LocalComment()); + $result = $commentParser->parse(); + $this->warnings = array_merge($this->warnings, $commentParser->getWarnings()); + if($result->isInvalid()) { + return $result; + } + return $result; + } + + private function validateEscaping() : Result + { + //Backslash found + if ($this->lexer->token['type'] !== EmailLexer::S_BACKSLASH) { + return new ValidEmail(); + } + + if ($this->lexer->isNextToken(EmailLexer::GENERIC)) { + return new InvalidEmail(new ExpectingATEXT('Found ATOM after escaping'), $this->lexer->token['value']); + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) { + return new ValidEmail(); + } + + return new ValidEmail(); + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Parser/PartParser.php b/plugins/email/vendor/egulias/email-validator/src/Parser/PartParser.php new file mode 100644 index 0000000..fd65fc5 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Parser/PartParser.php @@ -0,0 +1,63 @@ +lexer = $lexer; + } + + abstract public function parse() : Result; + + /** + * @return \Egulias\EmailValidator\Warning\Warning[] + */ + public function getWarnings() + { + return $this->warnings; + } + + protected function parseFWS() : Result + { + $foldingWS = new FoldingWhiteSpace($this->lexer); + $resultFWS = $foldingWS->parse(); + $this->warnings = array_merge($this->warnings, $foldingWS->getWarnings()); + return $resultFWS; + } + + protected function checkConsecutiveDots() : Result + { + if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new ConsecutiveDot(), $this->lexer->token['value']); + } + + return new ValidEmail(); + } + + protected function escaped() : bool + { + $previous = $this->lexer->getPrevious(); + + return $previous && $previous['type'] === EmailLexer::S_BACKSLASH + && + $this->lexer->token['type'] !== EmailLexer::GENERIC; + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/InvalidEmail.php b/plugins/email/vendor/egulias/email-validator/src/Result/InvalidEmail.php new file mode 100644 index 0000000..3d85e15 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/InvalidEmail.php @@ -0,0 +1,46 @@ +token = $token; + $this->reason = $reason; + } + + public function isValid(): bool + { + return false; + } + + public function isInvalid(): bool + { + return true; + } + + public function description(): string + { + return $this->reason->description() . " in char " . $this->token; + } + + public function code(): int + { + return $this->reason->code(); + } + + public function reason() : Reason + { + return $this->reason; + } + +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/MultipleErrors.php b/plugins/email/vendor/egulias/email-validator/src/Result/MultipleErrors.php new file mode 100644 index 0000000..5fa85af --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/MultipleErrors.php @@ -0,0 +1,56 @@ +reasons[$reason->code()] = $reason; + } + + /** + * @return Reason[] + */ + public function getReasons() : array + { + return $this->reasons; + } + + public function reason() : Reason + { + return 0 !== count($this->reasons) + ? current($this->reasons) + : new EmptyReason(); + } + + public function description() : string + { + $description = ''; + foreach($this->reasons as $reason) { + $description .= $reason->description() . PHP_EOL; + } + + return $description; + } + + public function code() : int + { + return 0; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php new file mode 100644 index 0000000..76015a2 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php @@ -0,0 +1,16 @@ +detailedDescription = $details; + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php new file mode 100644 index 0000000..55f44bb --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php @@ -0,0 +1,16 @@ +exception = $exception; + + } + public function code() : int + { + return 999; + } + + public function description() : string + { + return $this->exception->getMessage(); + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php new file mode 100644 index 0000000..07ea8d2 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php @@ -0,0 +1,16 @@ +detailedDescription; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php new file mode 100644 index 0000000..64f5f7c --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php @@ -0,0 +1,16 @@ +element = $element; + } + + public function code() : int + { + return 201; + } + + public function description() : string + { + return 'Unusual element found, wourld render invalid in majority of cases. Element found: ' . $this->element; + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/Result.php b/plugins/email/vendor/egulias/email-validator/src/Result/Result.php new file mode 100644 index 0000000..1e16bcc --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/Result.php @@ -0,0 +1,27 @@ +reason = new ReasonSpoofEmail(); + parent::__construct($this->reason, ''); + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Result/ValidEmail.php b/plugins/email/vendor/egulias/email-validator/src/Result/ValidEmail.php new file mode 100644 index 0000000..4f3693a --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Result/ValidEmail.php @@ -0,0 +1,27 @@ +dnsGetRecord = $dnsGetRecord; + } + + public function isValid(string $email, EmailLexer $emailLexer) : bool + { + // use the input to check DNS if we cannot extract something similar to a domain + $host = $email; + + // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email + if (false !== $lastAtPos = strrpos($email, '@')) { + $host = substr($email, $lastAtPos + 1); + } + + // Get the domain parts + $hostParts = explode('.', $host); + + $isLocalDomain = count($hostParts) <= 1; + $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], self::RESERVED_DNS_TOP_LEVEL_NAMES, true); + + // Exclude reserved top level DNS names + if ($isLocalDomain || $isReservedTopLevel) { + $this->error = new InvalidEmail(new LocalOrReservedDomain(), $host); + return false; + } + + return $this->checkDns($host); + } + + public function getError() : ?InvalidEmail + { + return $this->error; + } + + public function getWarnings() : array + { + return $this->warnings; + } + + /** + * @param string $host + * + * @return bool + */ + protected function checkDns($host) + { + $variant = INTL_IDNA_VARIANT_UTS46; + + $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.'; + + return $this->validateDnsRecords($host); + } + + + /** + * Validate the DNS records for given host. + * + * @param string $host A set of DNS records in the format returned by dns_get_record. + * + * @return bool True on success. + */ + private function validateDnsRecords($host) : bool + { + $dnsRecordsResult = $this->dnsGetRecord->getRecords($host, static::DNS_RECORD_TYPES_TO_CHECK); + + if ($dnsRecordsResult->withError()) { + $this->error = new InvalidEmail(new UnableToGetDNSRecord(), ''); + return false; + } + + $dnsRecords = $dnsRecordsResult->getRecords(); + + // No MX, A or AAAA DNS records + if ($dnsRecords === []) { + $this->error = new InvalidEmail(new ReasonNoDNSRecord(), ''); + return false; + } + + // For each DNS record + foreach ($dnsRecords as $dnsRecord) { + if (!$this->validateMXRecord($dnsRecord)) { + // No MX records (fallback to A or AAAA records) + if (empty($this->mxRecords)) { + $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); + } + return false; + } + } + return true; + } + + /** + * Validate an MX record + * + * @param array $dnsRecord Given DNS record. + * + * @return bool True if valid. + */ + private function validateMxRecord($dnsRecord) : bool + { + if (!isset($dnsRecord['type'])) { + $this->error = new InvalidEmail(new ReasonNoDNSRecord(), ''); + return false; + } + + if ($dnsRecord['type'] !== 'MX') { + return true; + } + + // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505) + if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') { + $this->error = new InvalidEmail(new DomainAcceptsNoMail(), ""); + return false; + } + + $this->mxRecords[] = $dnsRecord; + + return true; + } +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php b/plugins/email/vendor/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php new file mode 100644 index 0000000..ec6fe4b --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php @@ -0,0 +1,28 @@ +records = $records; + $this->error = $error; + } + + public function getRecords() : array + { + return $this->records; + } + + public function withError() : bool + { + return $this->error; + } + + +} \ No newline at end of file diff --git a/plugins/email/vendor/egulias/email-validator/src/Validation/EmailValidation.php b/plugins/email/vendor/egulias/email-validator/src/Validation/EmailValidation.php new file mode 100644 index 0000000..1bcc0a7 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Validation/EmailValidation.php @@ -0,0 +1,34 @@ +setChecks(Spoofchecker::SINGLE_SCRIPT); + + if ($checker->isSuspicious($email)) { + $this->error = new SpoofEmail(); + } + + return $this->error === null; + } + + /** + * @return InvalidEmail + */ + public function getError() : ?InvalidEmail + { + return $this->error; + } + + public function getWarnings() : array + { + return []; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Validation/MessageIDValidation.php b/plugins/email/vendor/egulias/email-validator/src/Validation/MessageIDValidation.php new file mode 100644 index 0000000..0e02043 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Validation/MessageIDValidation.php @@ -0,0 +1,51 @@ +parse($email); + $this->warnings = $parser->getWarnings(); + if ($result->isInvalid()) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->error = $result; + return false; + } + } catch (\Exception $invalid) { + $this->error = new InvalidEmail(new ExceptionFound($invalid), ''); + return false; + } + + return true; + } + + public function getWarnings(): array + { + return $this->warnings; + } + + public function getError(): ?InvalidEmail + { + return $this->error; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php b/plugins/email/vendor/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php new file mode 100644 index 0000000..90d4f0d --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php @@ -0,0 +1,117 @@ +validations = $validations; + $this->mode = $mode; + } + + /** + * {@inheritdoc} + */ + public function isValid(string $email, EmailLexer $emailLexer) : bool + { + $result = true; + foreach ($this->validations as $validation) { + $emailLexer->reset(); + $validationResult = $validation->isValid($email, $emailLexer); + $result = $result && $validationResult; + $this->warnings = array_merge($this->warnings, $validation->getWarnings()); + if (!$validationResult) { + $this->processError($validation); + } + + if ($this->shouldStop($result)) { + break; + } + } + + return $result; + } + + private function initErrorStorage() : void + { + if (null === $this->error) { + $this->error = new MultipleErrors(); + } + } + + private function processError(EmailValidation $validation) : void + { + if (null !== $validation->getError()) { + $this->initErrorStorage(); + /** @psalm-suppress PossiblyNullReference */ + $this->error->addReason($validation->getError()->reason()); + } + } + + private function shouldStop(bool $result) : bool + { + return !$result && $this->mode === self::STOP_ON_ERROR; + } + + /** + * Returns the validation errors. + */ + public function getError() : ?InvalidEmail + { + return $this->error; + } + + /** + * {@inheritdoc} + */ + public function getWarnings() : array + { + return $this->warnings; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php b/plugins/email/vendor/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php new file mode 100644 index 0000000..06885ed --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php @@ -0,0 +1,41 @@ +getWarnings())) { + return true; + } + + $this->error = new InvalidEmail(new RFCWarnings(), ''); + + return false; + } + + /** + * {@inheritdoc} + */ + public function getError() : ?InvalidEmail + { + return $this->error ?: parent::getError(); + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Validation/RFCValidation.php b/plugins/email/vendor/egulias/email-validator/src/Validation/RFCValidation.php new file mode 100644 index 0000000..e2c27ba --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Validation/RFCValidation.php @@ -0,0 +1,55 @@ +parser = new EmailParser($emailLexer); + try { + $result = $this->parser->parse($email); + $this->warnings = $this->parser->getWarnings(); + if ($result->isInvalid()) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->error = $result; + return false; + } + } catch (\Exception $invalid) { + $this->error = new InvalidEmail(new ExceptionFound($invalid), ''); + return false; + } + + return true; + } + + public function getError() : ?InvalidEmail + { + return $this->error; + } + + public function getWarnings() : array + { + return $this->warnings; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/AddressLiteral.php b/plugins/email/vendor/egulias/email-validator/src/Warning/AddressLiteral.php new file mode 100644 index 0000000..77e70f7 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/AddressLiteral.php @@ -0,0 +1,14 @@ +message = 'Address literal in domain part'; + $this->rfcNumber = 5321; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSNearAt.php b/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSNearAt.php new file mode 100644 index 0000000..be43bbe --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSNearAt.php @@ -0,0 +1,13 @@ +message = "Deprecated folding white space near @"; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSWithFWS.php b/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSWithFWS.php new file mode 100644 index 0000000..dea3450 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSWithFWS.php @@ -0,0 +1,13 @@ +message = 'Folding whites space followed by folding white space'; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/Comment.php b/plugins/email/vendor/egulias/email-validator/src/Warning/Comment.php new file mode 100644 index 0000000..704c290 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/Comment.php @@ -0,0 +1,13 @@ +message = "Comments found in this email"; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/DeprecatedComment.php b/plugins/email/vendor/egulias/email-validator/src/Warning/DeprecatedComment.php new file mode 100644 index 0000000..ad43bd7 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/DeprecatedComment.php @@ -0,0 +1,13 @@ +message = 'Deprecated comments'; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/DomainLiteral.php b/plugins/email/vendor/egulias/email-validator/src/Warning/DomainLiteral.php new file mode 100644 index 0000000..6f36b5e --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/DomainLiteral.php @@ -0,0 +1,14 @@ +message = 'Domain Literal'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/EmailTooLong.php b/plugins/email/vendor/egulias/email-validator/src/Warning/EmailTooLong.php new file mode 100644 index 0000000..497309d --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/EmailTooLong.php @@ -0,0 +1,15 @@ +message = 'Email is too long, exceeds ' . EmailParser::EMAIL_MAX_LENGTH; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6BadChar.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6BadChar.php new file mode 100644 index 0000000..ba2fcc0 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6BadChar.php @@ -0,0 +1,14 @@ +message = 'Bad char in IPV6 domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonEnd.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonEnd.php new file mode 100644 index 0000000..41afa78 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonEnd.php @@ -0,0 +1,14 @@ +message = ':: found at the end of the domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonStart.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonStart.php new file mode 100644 index 0000000..1bf754e --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonStart.php @@ -0,0 +1,14 @@ +message = ':: found at the start of the domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6Deprecated.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6Deprecated.php new file mode 100644 index 0000000..d752caa --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6Deprecated.php @@ -0,0 +1,14 @@ +message = 'Deprecated form of IPV6'; + $this->rfcNumber = 5321; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6DoubleColon.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6DoubleColon.php new file mode 100644 index 0000000..4f82394 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6DoubleColon.php @@ -0,0 +1,14 @@ +message = 'Double colon found after IPV6 tag'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6GroupCount.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6GroupCount.php new file mode 100644 index 0000000..a59d317 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6GroupCount.php @@ -0,0 +1,14 @@ +message = 'Group count is not IPV6 valid'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6MaxGroups.php b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6MaxGroups.php new file mode 100644 index 0000000..936274c --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6MaxGroups.php @@ -0,0 +1,14 @@ +message = 'Reached the maximum number of IPV6 groups allowed'; + $this->rfcNumber = 5321; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/LocalTooLong.php b/plugins/email/vendor/egulias/email-validator/src/Warning/LocalTooLong.php new file mode 100644 index 0000000..0d08d8b --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/LocalTooLong.php @@ -0,0 +1,15 @@ +message = 'Local part is too long, exceeds 64 chars (octets)'; + $this->rfcNumber = 5322; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/NoDNSMXRecord.php b/plugins/email/vendor/egulias/email-validator/src/Warning/NoDNSMXRecord.php new file mode 100644 index 0000000..b3c21a1 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/NoDNSMXRecord.php @@ -0,0 +1,14 @@ +message = 'No MX DSN record was found for this email'; + $this->rfcNumber = 5321; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/ObsoleteDTEXT.php b/plugins/email/vendor/egulias/email-validator/src/Warning/ObsoleteDTEXT.php new file mode 100644 index 0000000..10f19e3 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/ObsoleteDTEXT.php @@ -0,0 +1,14 @@ +rfcNumber = 5322; + $this->message = 'Obsolete DTEXT in domain literal'; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedPart.php b/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedPart.php new file mode 100644 index 0000000..36a4265 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedPart.php @@ -0,0 +1,17 @@ +message = "Deprecated Quoted String found between $prevToken and $postToken"; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedString.php b/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedString.php new file mode 100644 index 0000000..817e4e8 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedString.php @@ -0,0 +1,17 @@ +message = "Quoted String found between $prevToken and $postToken"; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/TLD.php b/plugins/email/vendor/egulias/email-validator/src/Warning/TLD.php new file mode 100644 index 0000000..2338b9f --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/TLD.php @@ -0,0 +1,13 @@ +message = "RFC5321, TLD"; + } +} diff --git a/plugins/email/vendor/egulias/email-validator/src/Warning/Warning.php b/plugins/email/vendor/egulias/email-validator/src/Warning/Warning.php new file mode 100644 index 0000000..a0158a2 --- /dev/null +++ b/plugins/email/vendor/egulias/email-validator/src/Warning/Warning.php @@ -0,0 +1,47 @@ +message; + } + + /** + * @return int + */ + public function code() + { + return self::CODE; + } + + /** + * @return int + */ + public function RFCNumber() + { + return $this->rfcNumber; + } + + public function __toString() + { + return $this->message() . " rfc: " . $this->rfcNumber . "internal code: " . static::CODE; + } +} diff --git a/plugins/email/vendor/psr/container/.gitignore b/plugins/email/vendor/psr/container/.gitignore new file mode 100644 index 0000000..b2395aa --- /dev/null +++ b/plugins/email/vendor/psr/container/.gitignore @@ -0,0 +1,3 @@ +composer.lock +composer.phar +/vendor/ diff --git a/plugins/email/vendor/psr/container/LICENSE b/plugins/email/vendor/psr/container/LICENSE new file mode 100644 index 0000000..2877a48 --- /dev/null +++ b/plugins/email/vendor/psr/container/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2016 container-interop +Copyright (c) 2016 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/email/vendor/psr/container/README.md b/plugins/email/vendor/psr/container/README.md new file mode 100644 index 0000000..1b9d9e5 --- /dev/null +++ b/plugins/email/vendor/psr/container/README.md @@ -0,0 +1,13 @@ +Container interface +============== + +This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url]. + +Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container. + +The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. + +[psr-url]: https://www.php-fig.org/psr/psr-11/ +[package-url]: https://packagist.org/packages/psr/container +[implementation-url]: https://packagist.org/providers/psr/container-implementation + diff --git a/plugins/email/vendor/psr/container/composer.json b/plugins/email/vendor/psr/container/composer.json new file mode 100644 index 0000000..3797a25 --- /dev/null +++ b/plugins/email/vendor/psr/container/composer.json @@ -0,0 +1,22 @@ +{ + "name": "psr/container", + "type": "library", + "description": "Common Container Interface (PHP FIG PSR-11)", + "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"], + "homepage": "https://github.com/php-fig/container", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=7.2.0" + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + } +} diff --git a/plugins/email/vendor/psr/container/src/ContainerExceptionInterface.php b/plugins/email/vendor/psr/container/src/ContainerExceptionInterface.php new file mode 100644 index 0000000..cf10b8b --- /dev/null +++ b/plugins/email/vendor/psr/container/src/ContainerExceptionInterface.php @@ -0,0 +1,10 @@ +=7.2.0" + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/plugins/email/vendor/psr/event-dispatcher/src/EventDispatcherInterface.php b/plugins/email/vendor/psr/event-dispatcher/src/EventDispatcherInterface.php new file mode 100644 index 0000000..4306fa9 --- /dev/null +++ b/plugins/email/vendor/psr/event-dispatcher/src/EventDispatcherInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +class AmqpFactory +{ + public function createConnection(array $credentials): \AMQPConnection + { + return new \AMQPConnection($credentials); + } + + public function createChannel(\AMQPConnection $connection): \AMQPChannel + { + return new \AMQPChannel($connection); + } + + public function createQueue(\AMQPChannel $channel): \AMQPQueue + { + return new \AMQPQueue($channel); + } + + public function createExchange(\AMQPChannel $channel): \AMQPExchange + { + return new \AMQPExchange($channel); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpFactory::class, false)) { + class_alias(AmqpFactory::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpFactory::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpReceivedStamp.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpReceivedStamp.php new file mode 100644 index 0000000..b661cd8 --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpReceivedStamp.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; + +/** + * Stamp applied when a message is received from Amqp. + */ +class AmqpReceivedStamp implements NonSendableStampInterface +{ + private $amqpEnvelope; + private $queueName; + + public function __construct(\AMQPEnvelope $amqpEnvelope, string $queueName) + { + $this->amqpEnvelope = $amqpEnvelope; + $this->queueName = $queueName; + } + + public function getAmqpEnvelope(): \AMQPEnvelope + { + return $this->amqpEnvelope; + } + + public function getQueueName(): string + { + return $this->queueName; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp::class, false)) { + class_alias(AmqpReceivedStamp::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpReceiver.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpReceiver.php new file mode 100644 index 0000000..141ab8c --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpReceiver.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; +use Symfony\Component\Messenger\Exception\TransportException; +use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; +use Symfony\Component\Messenger\Transport\Receiver\QueueReceiverInterface; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * Symfony Messenger receiver to get messages from AMQP brokers using PHP's AMQP extension. + * + * @author Samuel Roze + */ +class AmqpReceiver implements QueueReceiverInterface, MessageCountAwareInterface +{ + private $serializer; + private $connection; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + yield from $this->getFromQueues($this->connection->getQueueNames()); + } + + /** + * {@inheritdoc} + */ + public function getFromQueues(array $queueNames): iterable + { + foreach ($queueNames as $queueName) { + yield from $this->getEnvelope($queueName); + } + } + + private function getEnvelope(string $queueName): iterable + { + try { + $amqpEnvelope = $this->connection->get($queueName); + } catch (\AMQPException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + + if (null === $amqpEnvelope) { + return; + } + + $body = $amqpEnvelope->getBody(); + + try { + $envelope = $this->serializer->decode([ + 'body' => false === $body ? '' : $body, // workaround https://github.com/pdezwart/php-amqp/issues/351 + 'headers' => $amqpEnvelope->getHeaders(), + ]); + } catch (MessageDecodingFailedException $exception) { + // invalid message of some type + $this->rejectAmqpEnvelope($amqpEnvelope, $queueName); + + throw $exception; + } + + yield $envelope->with(new AmqpReceivedStamp($amqpEnvelope, $queueName)); + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + try { + $stamp = $this->findAmqpStamp($envelope); + + $this->connection->ack( + $stamp->getAmqpEnvelope(), + $stamp->getQueueName() + ); + } catch (\AMQPException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + $stamp = $this->findAmqpStamp($envelope); + + $this->rejectAmqpEnvelope( + $stamp->getAmqpEnvelope(), + $stamp->getQueueName() + ); + } + + /** + * {@inheritdoc} + */ + public function getMessageCount(): int + { + try { + return $this->connection->countMessagesInQueues(); + } catch (\AMQPException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + private function rejectAmqpEnvelope(\AMQPEnvelope $amqpEnvelope, string $queueName): void + { + try { + $this->connection->nack($amqpEnvelope, $queueName, \AMQP_NOPARAM); + } catch (\AMQPException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + private function findAmqpStamp(Envelope $envelope): AmqpReceivedStamp + { + $amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class); + if (null === $amqpReceivedStamp) { + throw new LogicException('No "AmqpReceivedStamp" stamp found on the Envelope.'); + } + + return $amqpReceivedStamp; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver::class, false)) { + class_alias(AmqpReceiver::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpSender.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpSender.php new file mode 100644 index 0000000..5fdfdff --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpSender.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\TransportException; +use Symfony\Component\Messenger\Stamp\DelayStamp; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; +use Symfony\Component\Messenger\Transport\Sender\SenderInterface; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * Symfony Messenger sender to send messages to AMQP brokers using PHP's AMQP extension. + * + * @author Samuel Roze + */ +class AmqpSender implements SenderInterface +{ + private $serializer; + private $connection; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + $encodedMessage = $this->serializer->encode($envelope); + + /** @var DelayStamp|null $delayStamp */ + $delayStamp = $envelope->last(DelayStamp::class); + $delay = $delayStamp ? $delayStamp->getDelay() : 0; + + /** @var AmqpStamp|null $amqpStamp */ + $amqpStamp = $envelope->last(AmqpStamp::class); + if (isset($encodedMessage['headers']['Content-Type'])) { + $contentType = $encodedMessage['headers']['Content-Type']; + unset($encodedMessage['headers']['Content-Type']); + + if (!$amqpStamp || !isset($amqpStamp->getAttributes()['content_type'])) { + $amqpStamp = AmqpStamp::createWithAttributes(['content_type' => $contentType], $amqpStamp); + } + } + + $amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class); + if ($amqpReceivedStamp instanceof AmqpReceivedStamp) { + $amqpStamp = AmqpStamp::createFromAmqpEnvelope( + $amqpReceivedStamp->getAmqpEnvelope(), + $amqpStamp, + $envelope->last(RedeliveryStamp::class) ? $amqpReceivedStamp->getQueueName() : null + ); + } + + try { + $this->connection->publish( + $encodedMessage['body'], + $encodedMessage['headers'] ?? [], + $delay, + $amqpStamp + ); + } catch (\AMQPException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + return $envelope; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender::class, false)) { + class_alias(AmqpSender::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpStamp.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpStamp.php new file mode 100644 index 0000000..5835bdc --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpStamp.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; + +/** + * @author Guillaume Gammelin + * @author Samuel Roze + */ +final class AmqpStamp implements NonSendableStampInterface +{ + private $routingKey; + private $flags; + private $attributes; + private $isRetryAttempt = false; + + public function __construct(string $routingKey = null, int $flags = \AMQP_NOPARAM, array $attributes = []) + { + $this->routingKey = $routingKey; + $this->flags = $flags; + $this->attributes = $attributes; + } + + public function getRoutingKey(): ?string + { + return $this->routingKey; + } + + public function getFlags(): int + { + return $this->flags; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public static function createFromAmqpEnvelope(\AMQPEnvelope $amqpEnvelope, self $previousStamp = null, string $retryRoutingKey = null): self + { + $attr = $previousStamp->attributes ?? []; + + $attr['headers'] = $attr['headers'] ?? $amqpEnvelope->getHeaders(); + $attr['content_type'] = $attr['content_type'] ?? $amqpEnvelope->getContentType(); + $attr['content_encoding'] = $attr['content_encoding'] ?? $amqpEnvelope->getContentEncoding(); + $attr['delivery_mode'] = $attr['delivery_mode'] ?? $amqpEnvelope->getDeliveryMode(); + $attr['priority'] = $attr['priority'] ?? $amqpEnvelope->getPriority(); + $attr['timestamp'] = $attr['timestamp'] ?? $amqpEnvelope->getTimestamp(); + $attr['app_id'] = $attr['app_id'] ?? $amqpEnvelope->getAppId(); + $attr['message_id'] = $attr['message_id'] ?? $amqpEnvelope->getMessageId(); + $attr['user_id'] = $attr['user_id'] ?? $amqpEnvelope->getUserId(); + $attr['expiration'] = $attr['expiration'] ?? $amqpEnvelope->getExpiration(); + $attr['type'] = $attr['type'] ?? $amqpEnvelope->getType(); + $attr['reply_to'] = $attr['reply_to'] ?? $amqpEnvelope->getReplyTo(); + $attr['correlation_id'] = $attr['correlation_id'] ?? $amqpEnvelope->getCorrelationId(); + + if (null === $retryRoutingKey) { + $stamp = new self($previousStamp->routingKey ?? $amqpEnvelope->getRoutingKey(), $previousStamp->flags ?? \AMQP_NOPARAM, $attr); + } else { + $stamp = new self($retryRoutingKey, $previousStamp->flags ?? \AMQP_NOPARAM, $attr); + $stamp->isRetryAttempt = true; + } + + return $stamp; + } + + public function isRetryAttempt(): bool + { + return $this->isRetryAttempt; + } + + public static function createWithAttributes(array $attributes, self $previousStamp = null): self + { + return new self( + $previousStamp->routingKey ?? null, + $previousStamp->flags ?? \AMQP_NOPARAM, + array_merge($previousStamp->attributes ?? [], $attributes) + ); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp::class, false)) { + class_alias(AmqpStamp::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpTransport.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpTransport.php new file mode 100644 index 0000000..9ffda47 --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpTransport.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; +use Symfony\Component\Messenger\Transport\Receiver\QueueReceiverInterface; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\SetupableTransportInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Nicolas Grekas + */ +class AmqpTransport implements QueueReceiverInterface, TransportInterface, SetupableTransportInterface, MessageCountAwareInterface +{ + private $serializer; + private $connection; + private $receiver; + private $sender; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + return ($this->receiver ?? $this->getReceiver())->get(); + } + + /** + * {@inheritdoc} + */ + public function getFromQueues(array $queueNames): iterable + { + return ($this->receiver ?? $this->getReceiver())->getFromQueues($queueNames); + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + ($this->receiver ?? $this->getReceiver())->ack($envelope); + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + ($this->receiver ?? $this->getReceiver())->reject($envelope); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + return ($this->sender ?? $this->getSender())->send($envelope); + } + + /** + * {@inheritdoc} + */ + public function setup(): void + { + $this->connection->setup(); + } + + /** + * {@inheritdoc} + */ + public function getMessageCount(): int + { + return ($this->receiver ?? $this->getReceiver())->getMessageCount(); + } + + private function getReceiver(): AmqpReceiver + { + return $this->receiver = new AmqpReceiver($this->connection, $this->serializer); + } + + private function getSender(): AmqpSender + { + return $this->sender = new AmqpSender($this->connection, $this->serializer); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport::class, false)) { + class_alias(AmqpTransport::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpTransportFactory.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpTransportFactory.php new file mode 100644 index 0000000..420cf7a --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/AmqpTransportFactory.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\TransportFactoryInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Samuel Roze + */ +class AmqpTransportFactory implements TransportFactoryInterface +{ + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + unset($options['transport_name']); + + return new AmqpTransport(Connection::fromDsn($dsn, $options), $serializer); + } + + public function supports(string $dsn, array $options): bool + { + return 0 === strpos($dsn, 'amqp://') || 0 === strpos($dsn, 'amqps://'); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class, false)) { + class_alias(AmqpTransportFactory::class, \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/Transport/Connection.php b/plugins/email/vendor/symfony/amqp-messenger/Transport/Connection.php new file mode 100644 index 0000000..e5a9d59 --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/Transport/Connection.php @@ -0,0 +1,583 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Amqp\Transport; + +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Exception\LogicException; + +/** + * An AMQP connection. + * + * @author Samuel Roze + * + * @final + */ +class Connection +{ + private const ARGUMENTS_AS_INTEGER = [ + 'x-delay', + 'x-expires', + 'x-max-length', + 'x-max-length-bytes', + 'x-max-priority', + 'x-message-ttl', + ]; + + private const AVAILABLE_OPTIONS = [ + 'host', + 'port', + 'vhost', + 'user', + 'login', + 'password', + 'queues', + 'exchange', + 'delay', + 'auto_setup', + 'prefetch_count', + 'retry', + 'persistent', + 'frame_max', + 'channel_max', + 'heartbeat', + 'read_timeout', + 'write_timeout', + 'confirm_timeout', + 'connect_timeout', + 'cacert', + 'cert', + 'key', + 'verify', + 'sasl_method', + ]; + + private const AVAILABLE_QUEUE_OPTIONS = [ + 'binding_keys', + 'binding_arguments', + 'flags', + 'arguments', + ]; + + private const AVAILABLE_EXCHANGE_OPTIONS = [ + 'name', + 'type', + 'default_publish_routing_key', + 'flags', + 'arguments', + ]; + + private $connectionOptions; + private $exchangeOptions; + private $queuesOptions; + private $amqpFactory; + private $autoSetupExchange; + private $autoSetupDelayExchange; + + /** + * @var \AMQPChannel|null + */ + private $amqpChannel; + + /** + * @var \AMQPExchange|null + */ + private $amqpExchange; + + /** + * @var \AMQPQueue[]|null + */ + private $amqpQueues = []; + + /** + * @var \AMQPExchange|null + */ + private $amqpDelayExchange; + + public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null) + { + if (!\extension_loaded('amqp')) { + throw new LogicException(sprintf('You cannot use the "%s" as the "amqp" extension is not installed.', __CLASS__)); + } + + $this->connectionOptions = array_replace_recursive([ + 'delay' => [ + 'exchange_name' => 'delays', + 'queue_name_pattern' => 'delay_%exchange_name%_%routing_key%_%delay%', + ], + ], $connectionOptions); + $this->autoSetupExchange = $this->autoSetupDelayExchange = $connectionOptions['auto_setup'] ?? true; + $this->exchangeOptions = $exchangeOptions; + $this->queuesOptions = $queuesOptions; + $this->amqpFactory = $amqpFactory ?? new AmqpFactory(); + } + + /** + * Creates a connection based on the DSN and options. + * + * Available options: + * + * * host: Hostname of the AMQP service + * * port: Port of the AMQP service + * * vhost: Virtual Host to use with the AMQP service + * * user|login: Username to use to connect the AMQP service + * * password: Password to use to connect to the AMQP service + * * read_timeout: Timeout in for income activity. Note: 0 or greater seconds. May be fractional. + * * write_timeout: Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. + * * connect_timeout: Connection timeout. Note: 0 or greater seconds. May be fractional. + * * confirm_timeout: Timeout in seconds for confirmation, if none specified transport will not wait for message confirmation. Note: 0 or greater seconds. May be fractional. + * * queues[name]: An array of queues, keyed by the name + * * binding_keys: The binding keys (if any) to bind to this queue + * * binding_arguments: Arguments to be used while binding the queue. + * * flags: Queue flags (Default: AMQP_DURABLE) + * * arguments: Extra arguments + * * exchange: + * * name: Name of the exchange + * * type: Type of exchange (Default: fanout) + * * default_publish_routing_key: Routing key to use when publishing, if none is specified on the message + * * flags: Exchange flags (Default: AMQP_DURABLE) + * * arguments: Extra arguments + * * delay: + * * queue_name_pattern: Pattern to use to create the queues (Default: "delay_%exchange_name%_%routing_key%_%delay%") + * * exchange_name: Name of the exchange to be used for the delayed/retried messages (Default: "delays") + * * auto_setup: Enable or not the auto-setup of queues and exchanges (Default: true) + * + * * Connection tuning options (see http://www.rabbitmq.com/amqp-0-9-1-reference.html#connection.tune for details): + * * channel_max: Specifies highest channel number that the server permits. 0 means standard extension limit + * (see PHP_AMQP_MAX_CHANNELS constant) + * * frame_max: The largest frame size that the server proposes for the connection, including frame header + * and end-byte. 0 means standard extension limit (depends on librabbimq default frame size limit) + * * heartbeat: The delay, in seconds, of the connection heartbeat that the server wants. + * 0 means the server does not want a heartbeat. Note, librabbitmq has limited heartbeat support, + * which means heartbeats checked only during blocking calls. + * + * TLS support (see https://www.rabbitmq.com/ssl.html for details): + * * cacert: Path to the CA cert file in PEM format. + * * cert: Path to the client certificate in PEM format. + * * key: Path to the client key in PEM format. + * * verify: Enable or disable peer verification. If peer verification is enabled then the common name in the + * server certificate must match the server name. Peer verification is enabled by default. + */ + public static function fromDsn(string $dsn, array $options = [], AmqpFactory $amqpFactory = null): self + { + if (false === $parsedUrl = parse_url($dsn)) { + // this is a valid URI that parse_url cannot handle when you want to pass all parameters as options + if (!\in_array($dsn, ['amqp://', 'amqps://'])) { + throw new InvalidArgumentException(sprintf('The given AMQP DSN "%s" is invalid.', $dsn)); + } + + $parsedUrl = []; + } + + $useAmqps = 0 === strpos($dsn, 'amqps://'); + $pathParts = isset($parsedUrl['path']) ? explode('/', trim($parsedUrl['path'], '/')) : []; + $exchangeName = $pathParts[1] ?? 'messages'; + parse_str($parsedUrl['query'] ?? '', $parsedQuery); + $port = $useAmqps ? 5671 : 5672; + + $amqpOptions = array_replace_recursive([ + 'host' => $parsedUrl['host'] ?? 'localhost', + 'port' => $parsedUrl['port'] ?? $port, + 'vhost' => isset($pathParts[0]) ? urldecode($pathParts[0]) : '/', + 'exchange' => [ + 'name' => $exchangeName, + ], + ], $options, $parsedQuery); + + self::validateOptions($amqpOptions); + + if (isset($parsedUrl['user'])) { + $amqpOptions['login'] = urldecode($parsedUrl['user']); + } + + if (isset($parsedUrl['pass'])) { + $amqpOptions['password'] = urldecode($parsedUrl['pass']); + } + + if (!isset($amqpOptions['queues'])) { + $amqpOptions['queues'][$exchangeName] = []; + } + + $exchangeOptions = $amqpOptions['exchange']; + $queuesOptions = $amqpOptions['queues']; + unset($amqpOptions['queues'], $amqpOptions['exchange']); + if (isset($amqpOptions['auto_setup'])) { + $amqpOptions['auto_setup'] = filter_var($amqpOptions['auto_setup'], \FILTER_VALIDATE_BOOLEAN); + } + + $queuesOptions = array_map(function ($queueOptions) { + if (!\is_array($queueOptions)) { + $queueOptions = []; + } + if (\is_array($queueOptions['arguments'] ?? false)) { + $queueOptions['arguments'] = self::normalizeQueueArguments($queueOptions['arguments']); + } + + return $queueOptions; + }, $queuesOptions); + + if (!$useAmqps) { + unset($amqpOptions['cacert'], $amqpOptions['cert'], $amqpOptions['key'], $amqpOptions['verify']); + } + + if ($useAmqps && !self::hasCaCertConfigured($amqpOptions)) { + throw new InvalidArgumentException('No CA certificate has been provided. Set "amqp.cacert" in your php.ini or pass the "cacert" parameter in the DSN to use SSL. Alternatively, you can use amqp:// to use without SSL.'); + } + + return new self($amqpOptions, $exchangeOptions, $queuesOptions, $amqpFactory); + } + + private static function validateOptions(array $options): void + { + if (0 < \count($invalidOptions = array_diff(array_keys($options), self::AVAILABLE_OPTIONS))) { + trigger_deprecation('symfony/messenger', '5.1', 'Invalid option(s) "%s" passed to the AMQP Messenger transport. Passing invalid options is deprecated.', implode('", "', $invalidOptions)); + } + + if (isset($options['prefetch_count'])) { + trigger_deprecation('symfony/messenger', '5.3', 'The "prefetch_count" option passed to the AMQP Messenger transport has no effect and should not be used.'); + } + + if (\is_array($options['queues'] ?? false)) { + foreach ($options['queues'] as $queue) { + if (!\is_array($queue)) { + continue; + } + + if (0 < \count($invalidQueueOptions = array_diff(array_keys($queue), self::AVAILABLE_QUEUE_OPTIONS))) { + trigger_deprecation('symfony/messenger', '5.1', 'Invalid queue option(s) "%s" passed to the AMQP Messenger transport. Passing invalid queue options is deprecated.', implode('", "', $invalidQueueOptions)); + } + } + } + + if (\is_array($options['exchange'] ?? false) + && 0 < \count($invalidExchangeOptions = array_diff(array_keys($options['exchange']), self::AVAILABLE_EXCHANGE_OPTIONS))) { + trigger_deprecation('symfony/messenger', '5.1', 'Invalid exchange option(s) "%s" passed to the AMQP Messenger transport. Passing invalid exchange options is deprecated.', implode('", "', $invalidExchangeOptions)); + } + } + + private static function normalizeQueueArguments(array $arguments): array + { + foreach (self::ARGUMENTS_AS_INTEGER as $key) { + if (!\array_key_exists($key, $arguments)) { + continue; + } + + if (!is_numeric($arguments[$key])) { + throw new InvalidArgumentException(sprintf('Integer expected for queue argument "%s", "%s" given.', $key, get_debug_type($arguments[$key]))); + } + + $arguments[$key] = (int) $arguments[$key]; + } + + return $arguments; + } + + private static function hasCaCertConfigured(array $amqpOptions): bool + { + return (isset($amqpOptions['cacert']) && '' !== $amqpOptions['cacert']) || '' !== \ini_get('amqp.cacert'); + } + + /** + * @throws \AMQPException + */ + public function publish(string $body, array $headers = [], int $delayInMs = 0, AmqpStamp $amqpStamp = null): void + { + $this->clearWhenDisconnected(); + + if ($this->autoSetupExchange) { + $this->setupExchangeAndQueues(); // also setup normal exchange for delayed messages so delay queue can DLX messages to it + } + + if (0 !== $delayInMs) { + $this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp); + + return; + } + + $this->publishOnExchange( + $this->exchange(), + $body, + $this->getRoutingKeyForMessage($amqpStamp), + $headers, + $amqpStamp + ); + } + + /** + * Returns an approximate count of the messages in defined queues. + */ + public function countMessagesInQueues(): int + { + return array_sum(array_map(function ($queueName) { + return $this->queue($queueName)->declareQueue(); + }, $this->getQueueNames())); + } + + /** + * @throws \AMQPException + */ + private function publishWithDelay(string $body, array $headers, int $delay, AmqpStamp $amqpStamp = null) + { + $routingKey = $this->getRoutingKeyForMessage($amqpStamp); + $isRetryAttempt = $amqpStamp ? $amqpStamp->isRetryAttempt() : false; + + $this->setupDelay($delay, $routingKey, $isRetryAttempt); + + $this->publishOnExchange( + $this->getDelayExchange(), + $body, + $this->getRoutingKeyForDelay($delay, $routingKey, $isRetryAttempt), + $headers, + $amqpStamp + ); + } + + private function publishOnExchange(\AMQPExchange $exchange, string $body, string $routingKey = null, array $headers = [], AmqpStamp $amqpStamp = null) + { + $attributes = $amqpStamp ? $amqpStamp->getAttributes() : []; + $attributes['headers'] = array_merge($attributes['headers'] ?? [], $headers); + $attributes['delivery_mode'] = $attributes['delivery_mode'] ?? 2; + $attributes['timestamp'] = $attributes['timestamp'] ?? time(); + + $exchange->publish( + $body, + $routingKey, + $amqpStamp ? $amqpStamp->getFlags() : \AMQP_NOPARAM, + $attributes + ); + + if ('' !== ($this->connectionOptions['confirm_timeout'] ?? '')) { + $this->channel()->waitForConfirm((float) $this->connectionOptions['confirm_timeout']); + } + } + + private function setupDelay(int $delay, ?string $routingKey, bool $isRetryAttempt) + { + if ($this->autoSetupDelayExchange) { + $this->setupDelayExchange(); + } + + $queue = $this->createDelayQueue($delay, $routingKey, $isRetryAttempt); + $queue->declareQueue(); // the delay queue always need to be declared because the name is dynamic and cannot be declared in advance + $queue->bind($this->connectionOptions['delay']['exchange_name'], $this->getRoutingKeyForDelay($delay, $routingKey, $isRetryAttempt)); + } + + private function getDelayExchange(): \AMQPExchange + { + if (null === $this->amqpDelayExchange) { + $this->amqpDelayExchange = $this->amqpFactory->createExchange($this->channel()); + $this->amqpDelayExchange->setName($this->connectionOptions['delay']['exchange_name']); + $this->amqpDelayExchange->setType(\AMQP_EX_TYPE_DIRECT); + $this->amqpDelayExchange->setFlags(\AMQP_DURABLE); + } + + return $this->amqpDelayExchange; + } + + /** + * Creates a delay queue that will delay for a certain amount of time. + * + * This works by setting message TTL for the delay and pointing + * the dead letter exchange to the original exchange. The result + * is that after the TTL, the message is sent to the dead-letter-exchange, + * which is the original exchange, resulting on it being put back into + * the original queue. + */ + private function createDelayQueue(int $delay, ?string $routingKey, bool $isRetryAttempt): \AMQPQueue + { + $queue = $this->amqpFactory->createQueue($this->channel()); + $queue->setName($this->getRoutingKeyForDelay($delay, $routingKey, $isRetryAttempt)); + $queue->setFlags(\AMQP_DURABLE); + $queue->setArguments([ + 'x-message-ttl' => $delay, + // delete the delay queue 10 seconds after the message expires + // publishing another message redeclares the queue which renews the lease + 'x-expires' => $delay + 10000, + // message should be broadcasted to all consumers during delay, but to only one queue during retry + // empty name is default direct exchange + 'x-dead-letter-exchange' => $isRetryAttempt ? '' : $this->exchangeOptions['name'], + // after being released from to DLX, make sure the original routing key will be used + // we must use an empty string instead of null for the argument to be picked up + 'x-dead-letter-routing-key' => $routingKey ?? '', + ]); + + return $queue; + } + + private function getRoutingKeyForDelay(int $delay, ?string $finalRoutingKey, bool $isRetryAttempt): string + { + $action = $isRetryAttempt ? '_retry' : '_delay'; + + return str_replace( + ['%delay%', '%exchange_name%', '%routing_key%'], + [$delay, $this->exchangeOptions['name'], $finalRoutingKey ?? ''], + $this->connectionOptions['delay']['queue_name_pattern'] + ).$action; + } + + /** + * Gets a message from the specified queue. + * + * @throws \AMQPException + */ + public function get(string $queueName): ?\AMQPEnvelope + { + $this->clearWhenDisconnected(); + + if ($this->autoSetupExchange) { + $this->setupExchangeAndQueues(); + } + + if (false !== $message = $this->queue($queueName)->get()) { + return $message; + } + + return null; + } + + public function ack(\AMQPEnvelope $message, string $queueName): bool + { + return $this->queue($queueName)->ack($message->getDeliveryTag()); + } + + public function nack(\AMQPEnvelope $message, string $queueName, int $flags = \AMQP_NOPARAM): bool + { + return $this->queue($queueName)->nack($message->getDeliveryTag(), $flags); + } + + public function setup(): void + { + $this->setupExchangeAndQueues(); + $this->setupDelayExchange(); + } + + private function setupExchangeAndQueues(): void + { + $this->exchange()->declareExchange(); + + foreach ($this->queuesOptions as $queueName => $queueConfig) { + $this->queue($queueName)->declareQueue(); + foreach ($queueConfig['binding_keys'] ?? [null] as $bindingKey) { + $this->queue($queueName)->bind($this->exchangeOptions['name'], $bindingKey, $queueConfig['binding_arguments'] ?? []); + } + } + $this->autoSetupExchange = false; + } + + private function setupDelayExchange(): void + { + $this->getDelayExchange()->declareExchange(); + $this->autoSetupDelayExchange = false; + } + + /** + * @return string[] + */ + public function getQueueNames(): array + { + return array_keys($this->queuesOptions); + } + + public function channel(): \AMQPChannel + { + if (null === $this->amqpChannel) { + $connection = $this->amqpFactory->createConnection($this->connectionOptions); + $connectMethod = 'true' === ($this->connectionOptions['persistent'] ?? 'false') ? 'pconnect' : 'connect'; + + try { + $connection->{$connectMethod}(); + } catch (\AMQPConnectionException $e) { + throw new \AMQPException('Could not connect to the AMQP server. Please verify the provided DSN.', 0, $e); + } + $this->amqpChannel = $this->amqpFactory->createChannel($connection); + + if ('' !== ($this->connectionOptions['confirm_timeout'] ?? '')) { + $this->amqpChannel->confirmSelect(); + $this->amqpChannel->setConfirmCallback( + static function (): bool { + return false; + }, + static function (): bool { + return false; + } + ); + } + } + + return $this->amqpChannel; + } + + public function queue(string $queueName): \AMQPQueue + { + if (!isset($this->amqpQueues[$queueName])) { + $queueConfig = $this->queuesOptions[$queueName]; + + $amqpQueue = $this->amqpFactory->createQueue($this->channel()); + $amqpQueue->setName($queueName); + $amqpQueue->setFlags($queueConfig['flags'] ?? \AMQP_DURABLE); + + if (isset($queueConfig['arguments'])) { + $amqpQueue->setArguments($queueConfig['arguments']); + } + + $this->amqpQueues[$queueName] = $amqpQueue; + } + + return $this->amqpQueues[$queueName]; + } + + public function exchange(): \AMQPExchange + { + if (null === $this->amqpExchange) { + $this->amqpExchange = $this->amqpFactory->createExchange($this->channel()); + $this->amqpExchange->setName($this->exchangeOptions['name']); + $this->amqpExchange->setType($this->exchangeOptions['type'] ?? \AMQP_EX_TYPE_FANOUT); + $this->amqpExchange->setFlags($this->exchangeOptions['flags'] ?? \AMQP_DURABLE); + + if (isset($this->exchangeOptions['arguments'])) { + $this->amqpExchange->setArguments($this->exchangeOptions['arguments']); + } + } + + return $this->amqpExchange; + } + + private function clearWhenDisconnected(): void + { + if (!$this->channel()->isConnected()) { + $this->amqpChannel = null; + $this->amqpQueues = []; + $this->amqpExchange = null; + $this->amqpDelayExchange = null; + } + } + + private function getDefaultPublishRoutingKey(): ?string + { + return $this->exchangeOptions['default_publish_routing_key'] ?? null; + } + + public function purgeQueues() + { + foreach ($this->getQueueNames() as $queueName) { + $this->queue($queueName)->purge(); + } + } + + private function getRoutingKeyForMessage(?AmqpStamp $amqpStamp): ?string + { + return (null !== $amqpStamp ? $amqpStamp->getRoutingKey() : null) ?? $this->getDefaultPublishRoutingKey(); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\Connection::class, false)) { + class_alias(Connection::class, \Symfony\Component\Messenger\Transport\AmqpExt\Connection::class); +} diff --git a/plugins/email/vendor/symfony/amqp-messenger/composer.json b/plugins/email/vendor/symfony/amqp-messenger/composer.json new file mode 100644 index 0000000..dd0ecf9 --- /dev/null +++ b/plugins/email/vendor/symfony/amqp-messenger/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/amqp-messenger", + "type": "symfony-messenger-bridge", + "description": "Symfony AMQP extension Messenger Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/messenger": "^5.3|^6.0" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/deprecation-contracts/.gitignore b/plugins/email/vendor/symfony/deprecation-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/plugins/email/vendor/symfony/deprecation-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/plugins/email/vendor/symfony/deprecation-contracts/CHANGELOG.md b/plugins/email/vendor/symfony/deprecation-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/plugins/email/vendor/symfony/deprecation-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/plugins/email/vendor/symfony/deprecation-contracts/LICENSE b/plugins/email/vendor/symfony/deprecation-contracts/LICENSE new file mode 100644 index 0000000..406242f --- /dev/null +++ b/plugins/email/vendor/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/deprecation-contracts/README.md b/plugins/email/vendor/symfony/deprecation-contracts/README.md new file mode 100644 index 0000000..4957933 --- /dev/null +++ b/plugins/email/vendor/symfony/deprecation-contracts/README.md @@ -0,0 +1,26 @@ +Symfony Deprecation Contracts +============================= + +A generic function and convention to trigger deprecation notices. + +This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. + +By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, +the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. + +The function requires at least 3 arguments: + - the name of the Composer package that is triggering the deprecation + - the version of the package that introduced the deprecation + - the message of the deprecation + - more arguments can be provided: they will be inserted in the message using `printf()` formatting + +Example: +```php +trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); +``` + +This will generate the following message: +`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` + +While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty +`function trigger_deprecation() {}` in your application. diff --git a/plugins/email/vendor/symfony/deprecation-contracts/composer.json b/plugins/email/vendor/symfony/deprecation-contracts/composer.json new file mode 100644 index 0000000..cc7cc12 --- /dev/null +++ b/plugins/email/vendor/symfony/deprecation-contracts/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/deprecation-contracts", + "type": "library", + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/plugins/email/vendor/symfony/deprecation-contracts/function.php b/plugins/email/vendor/symfony/deprecation-contracts/function.php new file mode 100644 index 0000000..d437150 --- /dev/null +++ b/plugins/email/vendor/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/CHANGELOG.md b/plugins/email/vendor/symfony/doctrine-messenger/CHANGELOG.md new file mode 100644 index 0000000..aaed248 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/CHANGELOG.md @@ -0,0 +1,8 @@ +CHANGELOG +========= + +5.1.0 +----- + + * Introduced the Doctrine bridge. + * Added support for PostgreSQL `LISTEN`/`NOTIFY`. diff --git a/plugins/email/vendor/symfony/doctrine-messenger/LICENSE b/plugins/email/vendor/symfony/doctrine-messenger/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/doctrine-messenger/README.md b/plugins/email/vendor/symfony/doctrine-messenger/README.md new file mode 100644 index 0000000..d29c9c3 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/README.md @@ -0,0 +1,12 @@ +Doctrine Messenger +================== + +Provides Doctrine integration for Symfony Messenger. + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/Connection.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/Connection.php new file mode 100644 index 0000000..b760df6 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/Connection.php @@ -0,0 +1,508 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Doctrine\DBAL\Connection as DBALConnection; +use Doctrine\DBAL\Driver\Exception as DriverException; +use Doctrine\DBAL\Driver\Result as DriverResult; +use Doctrine\DBAL\Exception as DBALException; +use Doctrine\DBAL\Exception\TableNotFoundException; +use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Query\QueryBuilder; +use Doctrine\DBAL\Result; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\Comparator; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\SchemaDiff; +use Doctrine\DBAL\Schema\Synchronizer\SchemaSynchronizer; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\Types; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Exception\TransportException; +use Symfony\Contracts\Service\ResetInterface; + +/** + * @internal since Symfony 5.1 + * + * @author Vincent Touzet + * @author Kévin Dunglas + */ +class Connection implements ResetInterface +{ + protected const TABLE_OPTION_NAME = '_symfony_messenger_table_name'; + + protected const DEFAULT_OPTIONS = [ + 'table_name' => 'messenger_messages', + 'queue_name' => 'default', + 'redeliver_timeout' => 3600, + 'auto_setup' => true, + ]; + + /** + * Configuration of the connection. + * + * Available options: + * + * * table_name: name of the table + * * connection: name of the Doctrine's entity manager + * * queue_name: name of the queue + * * redeliver_timeout: Timeout before redeliver messages still in handling state (i.e: delivered_at is not null and message is still in table). Default: 3600 + * * auto_setup: Whether the table should be created automatically during send / get. Default: true + */ + protected $configuration = []; + protected $driverConnection; + protected $queueEmptiedAt; + private $schemaSynchronizer; + private $autoSetup; + + public function __construct(array $configuration, DBALConnection $driverConnection, SchemaSynchronizer $schemaSynchronizer = null) + { + $this->configuration = array_replace_recursive(static::DEFAULT_OPTIONS, $configuration); + $this->driverConnection = $driverConnection; + $this->schemaSynchronizer = $schemaSynchronizer; + $this->autoSetup = $this->configuration['auto_setup']; + } + + public function reset() + { + $this->queueEmptiedAt = null; + } + + public function getConfiguration(): array + { + return $this->configuration; + } + + public static function buildConfiguration(string $dsn, array $options = []): array + { + if (false === $components = parse_url($dsn)) { + throw new InvalidArgumentException(sprintf('The given Doctrine Messenger DSN "%s" is invalid.', $dsn)); + } + + $query = []; + if (isset($components['query'])) { + parse_str($components['query'], $query); + } + + $configuration = ['connection' => $components['host']]; + $configuration += $query + $options + static::DEFAULT_OPTIONS; + + $configuration['auto_setup'] = filter_var($configuration['auto_setup'], \FILTER_VALIDATE_BOOLEAN); + + // check for extra keys in options + $optionsExtraKeys = array_diff(array_keys($options), array_keys(static::DEFAULT_OPTIONS)); + if (0 < \count($optionsExtraKeys)) { + throw new InvalidArgumentException(sprintf('Unknown option found: [%s]. Allowed options are [%s].', implode(', ', $optionsExtraKeys), implode(', ', array_keys(static::DEFAULT_OPTIONS)))); + } + + // check for extra keys in options + $queryExtraKeys = array_diff(array_keys($query), array_keys(static::DEFAULT_OPTIONS)); + if (0 < \count($queryExtraKeys)) { + throw new InvalidArgumentException(sprintf('Unknown option found in DSN: [%s]. Allowed options are [%s].', implode(', ', $queryExtraKeys), implode(', ', array_keys(static::DEFAULT_OPTIONS)))); + } + + return $configuration; + } + + /** + * @param int $delay The delay in milliseconds + * + * @return string The inserted id + * + * @throws DBALException + */ + public function send(string $body, array $headers, int $delay = 0): string + { + $now = new \DateTime(); + $availableAt = (clone $now)->modify(sprintf('+%d seconds', $delay / 1000)); + + $queryBuilder = $this->driverConnection->createQueryBuilder() + ->insert($this->configuration['table_name']) + ->values([ + 'body' => '?', + 'headers' => '?', + 'queue_name' => '?', + 'created_at' => '?', + 'available_at' => '?', + ]); + + $this->executeStatement($queryBuilder->getSQL(), [ + $body, + json_encode($headers), + $this->configuration['queue_name'], + $now, + $availableAt, + ], [ + null, + null, + null, + Types::DATETIME_MUTABLE, + Types::DATETIME_MUTABLE, + ]); + + return $this->driverConnection->lastInsertId(); + } + + public function get(): ?array + { + if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { + try { + $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']); + } catch (DriverException $e) { + // Ignore the exception + } + } + + get: + $this->driverConnection->beginTransaction(); + try { + $query = $this->createAvailableMessagesQueryBuilder() + ->orderBy('available_at', 'ASC') + ->setMaxResults(1); + + // Append pessimistic write lock to FROM clause if db platform supports it + $sql = $query->getSQL(); + if (($fromPart = $query->getQueryPart('from')) && + ($table = $fromPart[0]['table'] ?? null) && + ($alias = $fromPart[0]['alias'] ?? null) + ) { + $fromClause = sprintf('%s %s', $table, $alias); + $sql = str_replace( + sprintf('FROM %s WHERE', $fromClause), + sprintf('FROM %s WHERE', $this->driverConnection->getDatabasePlatform()->appendLockHint($fromClause, LockMode::PESSIMISTIC_WRITE)), + $sql + ); + } + + // Wrap the rownum query in a sub-query to allow writelocks without ORA-02014 error + if ($this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) { + $sql = str_replace('SELECT a.* FROM', 'SELECT a.id FROM', $sql); + + $wrappedQuery = $this->driverConnection->createQueryBuilder() + ->select( + 'w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", '. + 'w.created_at AS "created_at", w.available_at AS "available_at", '. + 'w.delivered_at AS "delivered_at"' + ) + ->from($this->configuration['table_name'], 'w') + ->where('w.id IN('.$sql.')'); + + $sql = $wrappedQuery->getSQL(); + } + + // use SELECT ... FOR UPDATE to lock table + $stmt = $this->executeQuery( + $sql.' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(), + $query->getParameters(), + $query->getParameterTypes() + ); + $doctrineEnvelope = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch(); + + if (false === $doctrineEnvelope) { + $this->driverConnection->commit(); + $this->queueEmptiedAt = microtime(true) * 1000; + + return null; + } + // Postgres can "group" notifications having the same channel and payload + // We need to be sure to empty the queue before blocking again + $this->queueEmptiedAt = null; + + $doctrineEnvelope = $this->decodeEnvelopeHeaders($doctrineEnvelope); + + $queryBuilder = $this->driverConnection->createQueryBuilder() + ->update($this->configuration['table_name']) + ->set('delivered_at', '?') + ->where('id = ?'); + $now = new \DateTime(); + $this->executeStatement($queryBuilder->getSQL(), [ + $now, + $doctrineEnvelope['id'], + ], [ + Types::DATETIME_MUTABLE, + ]); + + $this->driverConnection->commit(); + + return $doctrineEnvelope; + } catch (\Throwable $e) { + $this->driverConnection->rollBack(); + + if ($this->autoSetup && $e instanceof TableNotFoundException) { + $this->setup(); + goto get; + } + + throw $e; + } + } + + public function ack(string $id): bool + { + try { + if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { + return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0; + } + + return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + public function reject(string $id): bool + { + try { + if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { + return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0; + } + + return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + public function setup(): void + { + $configuration = $this->driverConnection->getConfiguration(); + $assetFilter = $configuration->getSchemaAssetsFilter(); + $configuration->setSchemaAssetsFilter(null); + $this->updateSchema(); + $configuration->setSchemaAssetsFilter($assetFilter); + $this->autoSetup = false; + } + + public function getMessageCount(): int + { + $queryBuilder = $this->createAvailableMessagesQueryBuilder() + ->select('COUNT(m.id) as message_count') + ->setMaxResults(1); + + $stmt = $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes()); + + return $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchOne() : $stmt->fetchColumn(); + } + + public function findAll(int $limit = null): array + { + $queryBuilder = $this->createAvailableMessagesQueryBuilder(); + if (null !== $limit) { + $queryBuilder->setMaxResults($limit); + } + + $stmt = $this->executeQuery($queryBuilder->getSQL(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes()); + $data = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAllAssociative() : $stmt->fetchAll(); + + return array_map(function ($doctrineEnvelope) { + return $this->decodeEnvelopeHeaders($doctrineEnvelope); + }, $data); + } + + public function find($id): ?array + { + $queryBuilder = $this->createQueryBuilder() + ->where('m.id = ? and m.queue_name = ?'); + + $stmt = $this->executeQuery($queryBuilder->getSQL(), [$id, $this->configuration['queue_name']]); + $data = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch(); + + return false === $data ? null : $this->decodeEnvelopeHeaders($data); + } + + /** + * @internal + */ + public function configureSchema(Schema $schema, DBALConnection $forConnection): void + { + // only update the schema for this connection + if ($forConnection !== $this->driverConnection) { + return; + } + + if ($schema->hasTable($this->configuration['table_name'])) { + return; + } + + $this->addTableToSchema($schema); + } + + /** + * @internal + */ + public function getExtraSetupSqlForTable(Table $createdTable): array + { + return []; + } + + private function createAvailableMessagesQueryBuilder(): QueryBuilder + { + $now = new \DateTime(); + $redeliverLimit = (clone $now)->modify(sprintf('-%d seconds', $this->configuration['redeliver_timeout'])); + + return $this->createQueryBuilder() + ->where('m.delivered_at is null OR m.delivered_at < ?') + ->andWhere('m.available_at <= ?') + ->andWhere('m.queue_name = ?') + ->setParameters([ + $redeliverLimit, + $now, + $this->configuration['queue_name'], + ], [ + Types::DATETIME_MUTABLE, + Types::DATETIME_MUTABLE, + ]); + } + + private function createQueryBuilder(): QueryBuilder + { + return $this->driverConnection->createQueryBuilder() + ->select('m.*') + ->from($this->configuration['table_name'], 'm'); + } + + private function executeQuery(string $sql, array $parameters = [], array $types = []) + { + try { + $stmt = $this->driverConnection->executeQuery($sql, $parameters, $types); + } catch (TableNotFoundException $e) { + if ($this->driverConnection->isTransactionActive()) { + throw $e; + } + + // create table + if ($this->autoSetup) { + $this->setup(); + } + $stmt = $this->driverConnection->executeQuery($sql, $parameters, $types); + } + + return $stmt; + } + + protected function executeStatement(string $sql, array $parameters = [], array $types = []) + { + try { + if (method_exists($this->driverConnection, 'executeStatement')) { + $stmt = $this->driverConnection->executeStatement($sql, $parameters, $types); + } else { + $stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types); + } + } catch (TableNotFoundException $e) { + if ($this->driverConnection->isTransactionActive()) { + throw $e; + } + + // create table + if ($this->autoSetup) { + $this->setup(); + } + if (method_exists($this->driverConnection, 'executeStatement')) { + $stmt = $this->driverConnection->executeStatement($sql, $parameters, $types); + } else { + $stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types); + } + } + + return $stmt; + } + + private function getSchema(): Schema + { + $schema = new Schema([], [], $this->createSchemaManager()->createSchemaConfig()); + $this->addTableToSchema($schema); + + return $schema; + } + + private function addTableToSchema(Schema $schema): void + { + $table = $schema->createTable($this->configuration['table_name']); + // add an internal option to mark that we created this & the non-namespaced table name + $table->addOption(self::TABLE_OPTION_NAME, $this->configuration['table_name']); + $table->addColumn('id', Types::BIGINT) + ->setAutoincrement(true) + ->setNotnull(true); + $table->addColumn('body', Types::TEXT) + ->setNotnull(true); + $table->addColumn('headers', Types::TEXT) + ->setNotnull(true); + $table->addColumn('queue_name', Types::STRING) + ->setLength(190) // MySQL 5.6 only supports 191 characters on an indexed column in utf8mb4 mode + ->setNotnull(true); + $table->addColumn('created_at', Types::DATETIME_MUTABLE) + ->setNotnull(true); + $table->addColumn('available_at', Types::DATETIME_MUTABLE) + ->setNotnull(true); + $table->addColumn('delivered_at', Types::DATETIME_MUTABLE) + ->setNotnull(false); + $table->setPrimaryKey(['id']); + $table->addIndex(['queue_name']); + $table->addIndex(['available_at']); + $table->addIndex(['delivered_at']); + } + + private function decodeEnvelopeHeaders(array $doctrineEnvelope): array + { + $doctrineEnvelope['headers'] = json_decode($doctrineEnvelope['headers'], true); + + return $doctrineEnvelope; + } + + private function updateSchema(): void + { + if (null !== $this->schemaSynchronizer) { + $this->schemaSynchronizer->updateSchema($this->getSchema(), true); + + return; + } + + $schemaManager = $this->createSchemaManager(); + $comparator = $this->createComparator($schemaManager); + $schemaDiff = $this->compareSchemas($comparator, $schemaManager->createSchema(), $this->getSchema()); + + foreach ($schemaDiff->toSaveSql($this->driverConnection->getDatabasePlatform()) as $sql) { + if (method_exists($this->driverConnection, 'executeStatement')) { + $this->driverConnection->executeStatement($sql); + } else { + $this->driverConnection->exec($sql); + } + } + } + + private function createSchemaManager(): AbstractSchemaManager + { + return method_exists($this->driverConnection, 'createSchemaManager') + ? $this->driverConnection->createSchemaManager() + : $this->driverConnection->getSchemaManager(); + } + + private function createComparator(AbstractSchemaManager $schemaManager): Comparator + { + return method_exists($schemaManager, 'createComparator') + ? $schemaManager->createComparator() + : new Comparator(); + } + + private function compareSchemas(Comparator $comparator, Schema $from, Schema $to): SchemaDiff + { + return method_exists($comparator, 'compareSchemas') + ? $comparator->compareSchemas($from, $to) + : $comparator->compare($from, $to); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\Connection::class, false)) { + class_alias(Connection::class, \Symfony\Component\Messenger\Transport\Doctrine\Connection::class); +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineReceivedStamp.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineReceivedStamp.php new file mode 100644 index 0000000..7f0dee1 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineReceivedStamp.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; + +/** + * @author Vincent Touzet + */ +class DoctrineReceivedStamp implements NonSendableStampInterface +{ + private $id; + + public function __construct(string $id) + { + $this->id = $id; + } + + public function getId(): string + { + return $this->id; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceivedStamp::class, false)) { + class_alias(DoctrineReceivedStamp::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceivedStamp::class); +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineReceiver.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineReceiver.php new file mode 100644 index 0000000..69cf0ed --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineReceiver.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Doctrine\DBAL\Exception as DBALException; +use Doctrine\DBAL\Exception\RetryableException; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; +use Symfony\Component\Messenger\Exception\TransportException; +use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; +use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * @author Vincent Touzet + */ +class DoctrineReceiver implements ListableReceiverInterface, MessageCountAwareInterface +{ + private const MAX_RETRIES = 3; + private $retryingSafetyCounter = 0; + private $connection; + private $serializer; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + try { + $doctrineEnvelope = $this->connection->get(); + $this->retryingSafetyCounter = 0; // reset counter + } catch (RetryableException $exception) { + // Do nothing when RetryableException occurs less than "MAX_RETRIES" + // as it will likely be resolved on the next call to get() + // Problem with concurrent consumers and database deadlocks + if (++$this->retryingSafetyCounter >= self::MAX_RETRIES) { + $this->retryingSafetyCounter = 0; // reset counter + throw new TransportException($exception->getMessage(), 0, $exception); + } + + return []; + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + + if (null === $doctrineEnvelope) { + return []; + } + + return [$this->createEnvelopeFromData($doctrineEnvelope)]; + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + try { + $this->connection->ack($this->findDoctrineReceivedStamp($envelope)->getId()); + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + try { + $this->connection->reject($this->findDoctrineReceivedStamp($envelope)->getId()); + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + /** + * {@inheritdoc} + */ + public function getMessageCount(): int + { + try { + return $this->connection->getMessageCount(); + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } + + /** + * {@inheritdoc} + */ + public function all(int $limit = null): iterable + { + try { + $doctrineEnvelopes = $this->connection->findAll($limit); + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + + foreach ($doctrineEnvelopes as $doctrineEnvelope) { + yield $this->createEnvelopeFromData($doctrineEnvelope); + } + } + + /** + * {@inheritdoc} + */ + public function find($id): ?Envelope + { + try { + $doctrineEnvelope = $this->connection->find($id); + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + + if (null === $doctrineEnvelope) { + return null; + } + + return $this->createEnvelopeFromData($doctrineEnvelope); + } + + private function findDoctrineReceivedStamp(Envelope $envelope): DoctrineReceivedStamp + { + /** @var DoctrineReceivedStamp|null $doctrineReceivedStamp */ + $doctrineReceivedStamp = $envelope->last(DoctrineReceivedStamp::class); + + if (null === $doctrineReceivedStamp) { + throw new LogicException('No DoctrineReceivedStamp found on the Envelope.'); + } + + return $doctrineReceivedStamp; + } + + private function createEnvelopeFromData(array $data): Envelope + { + try { + $envelope = $this->serializer->decode([ + 'body' => $data['body'], + 'headers' => $data['headers'], + ]); + } catch (MessageDecodingFailedException $exception) { + $this->connection->reject($data['id']); + + throw $exception; + } + + return $envelope->with( + new DoctrineReceivedStamp($data['id']), + new TransportMessageIdStamp($data['id']) + ); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceiver::class, false)) { + class_alias(DoctrineReceiver::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineReceiver::class); +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineSender.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineSender.php new file mode 100644 index 0000000..6e5aa60 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineSender.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Doctrine\DBAL\Exception as DBALException; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\TransportException; +use Symfony\Component\Messenger\Stamp\DelayStamp; +use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; +use Symfony\Component\Messenger\Transport\Sender\SenderInterface; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * @author Vincent Touzet + */ +class DoctrineSender implements SenderInterface +{ + private $connection; + private $serializer; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + $encodedMessage = $this->serializer->encode($envelope); + + /** @var DelayStamp|null $delayStamp */ + $delayStamp = $envelope->last(DelayStamp::class); + $delay = null !== $delayStamp ? $delayStamp->getDelay() : 0; + + try { + $id = $this->connection->send($encodedMessage['body'], $encodedMessage['headers'] ?? [], $delay); + } catch (DBALException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + + return $envelope->with(new TransportMessageIdStamp($id)); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineSender::class, false)) { + class_alias(DoctrineSender::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineSender::class); +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineTransport.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineTransport.php new file mode 100644 index 0000000..97bc6fa --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineTransport.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Doctrine\DBAL\Connection as DbalConnection; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\SetupableTransportInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Vincent Touzet + */ +class DoctrineTransport implements TransportInterface, SetupableTransportInterface, MessageCountAwareInterface, ListableReceiverInterface +{ + private $connection; + private $serializer; + private $receiver; + private $sender; + + public function __construct(Connection $connection, SerializerInterface $serializer) + { + $this->connection = $connection; + $this->serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + return ($this->receiver ?? $this->getReceiver())->get(); + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + ($this->receiver ?? $this->getReceiver())->ack($envelope); + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + ($this->receiver ?? $this->getReceiver())->reject($envelope); + } + + /** + * {@inheritdoc} + */ + public function getMessageCount(): int + { + return ($this->receiver ?? $this->getReceiver())->getMessageCount(); + } + + /** + * {@inheritdoc} + */ + public function all(int $limit = null): iterable + { + return ($this->receiver ?? $this->getReceiver())->all($limit); + } + + /** + * {@inheritdoc} + */ + public function find($id): ?Envelope + { + return ($this->receiver ?? $this->getReceiver())->find($id); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + return ($this->sender ?? $this->getSender())->send($envelope); + } + + /** + * {@inheritdoc} + */ + public function setup(): void + { + $this->connection->setup(); + } + + /** + * Adds the Table to the Schema if this transport uses this connection. + */ + public function configureSchema(Schema $schema, DbalConnection $forConnection): void + { + $this->connection->configureSchema($schema, $forConnection); + } + + /** + * Adds extra SQL if the given table was created by the Connection. + * + * @return string[] + */ + public function getExtraSetupSqlForTable(Table $createdTable): array + { + return $this->connection->getExtraSetupSqlForTable($createdTable); + } + + private function getReceiver(): DoctrineReceiver + { + return $this->receiver = new DoctrineReceiver($this->connection, $this->serializer); + } + + private function getSender(): DoctrineSender + { + return $this->sender = new DoctrineSender($this->connection, $this->serializer); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport::class, false)) { + class_alias(DoctrineTransport::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport::class); +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineTransportFactory.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineTransportFactory.php new file mode 100644 index 0000000..f634e55 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/DoctrineTransportFactory.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\Persistence\ConnectionRegistry; +use Symfony\Bridge\Doctrine\RegistryInterface; +use Symfony\Component\Messenger\Exception\TransportException; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\TransportFactoryInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Vincent Touzet + */ +class DoctrineTransportFactory implements TransportFactoryInterface +{ + private $registry; + + public function __construct($registry) + { + if (!$registry instanceof RegistryInterface && !$registry instanceof ConnectionRegistry) { + throw new \TypeError(sprintf('Expected an instance of "%s" or "%s", but got "%s".', RegistryInterface::class, ConnectionRegistry::class, get_debug_type($registry))); + } + + $this->registry = $registry; + } + + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + $useNotify = ($options['use_notify'] ?? true); + unset($options['transport_name'], $options['use_notify']); + // Always allow PostgreSQL-specific keys, to be able to transparently fallback to the native driver when LISTEN/NOTIFY isn't available + $configuration = PostgreSqlConnection::buildConfiguration($dsn, $options); + + try { + $driverConnection = $this->registry->getConnection($configuration['connection']); + } catch (\InvalidArgumentException $e) { + throw new TransportException(sprintf('Could not find Doctrine connection from Messenger DSN "%s".', $dsn), 0, $e); + } + + if ($useNotify && $driverConnection->getDatabasePlatform() instanceof PostgreSQLPlatform) { + $connection = new PostgreSqlConnection($configuration, $driverConnection); + } else { + $connection = new Connection($configuration, $driverConnection); + } + + return new DoctrineTransport($connection, $serializer); + } + + public function supports(string $dsn, array $options): bool + { + return 0 === strpos($dsn, 'doctrine://'); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory::class, false)) { + class_alias(DoctrineTransportFactory::class, \Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory::class); +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/Transport/PostgreSqlConnection.php b/plugins/email/vendor/symfony/doctrine-messenger/Transport/PostgreSqlConnection.php new file mode 100644 index 0000000..3691a93 --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/Transport/PostgreSqlConnection.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; + +use Doctrine\DBAL\Schema\Table; + +/** + * Uses PostgreSQL LISTEN/NOTIFY to push messages to workers. + * + * @internal + * + * @author Kévin Dunglas + */ +final class PostgreSqlConnection extends Connection +{ + /** + * * use_notify: Set to false to disable the use of LISTEN/NOTIFY. Default: true + * * check_delayed_interval: The interval to check for delayed messages, in milliseconds. Set to 0 to disable checks. Default: 60000 (1 minute) + * * get_notify_timeout: The length of time to wait for a response when calling PDO::pgsqlGetNotify, in milliseconds. Default: 0. + */ + protected const DEFAULT_OPTIONS = parent::DEFAULT_OPTIONS + [ + 'use_notify' => true, + 'check_delayed_interval' => 60000, + 'get_notify_timeout' => 0, + ]; + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->unlisten(); + } + + public function reset() + { + parent::reset(); + $this->unlisten(); + } + + public function get(): ?array + { + if (null === $this->queueEmptiedAt) { + return parent::get(); + } + + // This is secure because the table name must be a valid identifier: + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name'])); + + if (method_exists($this->driverConnection, 'getNativeConnection')) { + $wrappedConnection = $this->driverConnection->getNativeConnection(); + } else { + $wrappedConnection = $this->driverConnection; + while (method_exists($wrappedConnection, 'getWrappedConnection')) { + $wrappedConnection = $wrappedConnection->getWrappedConnection(); + } + } + + $notification = $wrappedConnection->pgsqlGetNotify(\PDO::FETCH_ASSOC, $this->configuration['get_notify_timeout']); + if ( + // no notifications, or for another table or queue + (false === $notification || $notification['message'] !== $this->configuration['table_name'] || $notification['payload'] !== $this->configuration['queue_name']) && + // delayed messages + (microtime(true) * 1000 - $this->queueEmptiedAt < $this->configuration['check_delayed_interval']) + ) { + usleep(1000); + + return null; + } + + return parent::get(); + } + + public function setup(): void + { + parent::setup(); + + $this->executeStatement(implode("\n", $this->getTriggerSql())); + } + + /** + * @return string[] + */ + public function getExtraSetupSqlForTable(Table $createdTable): array + { + if (!$createdTable->hasOption(self::TABLE_OPTION_NAME)) { + return []; + } + + if ($createdTable->getOption(self::TABLE_OPTION_NAME) !== $this->configuration['table_name']) { + return []; + } + + return $this->getTriggerSql(); + } + + private function getTriggerSql(): array + { + $functionName = $this->createTriggerFunctionName(); + + return [ + // create trigger function + sprintf(<<<'SQL' +CREATE OR REPLACE FUNCTION %1$s() RETURNS TRIGGER AS $$ + BEGIN + PERFORM pg_notify('%2$s', NEW.queue_name::text); + RETURN NEW; + END; +$$ LANGUAGE plpgsql; +SQL + , $functionName, $this->configuration['table_name']), + // register trigger + sprintf('DROP TRIGGER IF EXISTS notify_trigger ON %s;', $this->configuration['table_name']), + sprintf('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON %1$s FOR EACH ROW EXECUTE PROCEDURE %2$s();', $this->configuration['table_name'], $functionName), + ]; + } + + private function createTriggerFunctionName(): string + { + $tableConfig = explode('.', $this->configuration['table_name']); + + if (1 === \count($tableConfig)) { + return sprintf('notify_%1$s', $tableConfig[0]); + } + + return sprintf('%1$s.notify_%2$s', $tableConfig[0], $tableConfig[1]); + } + + private function unlisten() + { + $this->executeStatement(sprintf('UNLISTEN "%s"', $this->configuration['table_name'])); + } +} diff --git a/plugins/email/vendor/symfony/doctrine-messenger/composer.json b/plugins/email/vendor/symfony/doctrine-messenger/composer.json new file mode 100644 index 0000000..3a9494a --- /dev/null +++ b/plugins/email/vendor/symfony/doctrine-messenger/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/doctrine-messenger", + "type": "symfony-messenger-bridge", + "description": "Symfony Doctrine Messenger Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/messenger": "^5.1|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "require-dev": { + "doctrine/dbal": "^2.13|^3.0", + "doctrine/persistence": "^1.3|^2|^3", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13", + "doctrine/persistence": "<1.3" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/.gitignore b/plugins/email/vendor/symfony/event-dispatcher-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md b/plugins/email/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/Event.php b/plugins/email/vendor/symfony/event-dispatcher-contracts/Event.php new file mode 100644 index 0000000..46dcb2b --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/Event.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; + +/** + * Event is the base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass + * state information to an event handler when an event is raised. + * + * You can call the method stopPropagation() to abort the execution of + * further listeners in your event listener. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Nicolas Grekas + */ +class Event implements StoppableEventInterface +{ + private $propagationStopped = false; + + /** + * {@inheritdoc} + */ + public function isPropagationStopped(): bool + { + return $this->propagationStopped; + } + + /** + * Stops the propagation of the event to further event listeners. + * + * If multiple event listeners are connected to the same event, no + * further event listener will be triggered once any trigger calls + * stopPropagation(). + */ + public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php b/plugins/email/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php new file mode 100644 index 0000000..351dc51 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; + +/** + * Allows providing hooks on domain-specific lifecycles by dispatching events. + */ +interface EventDispatcherInterface extends PsrEventDispatcherInterface +{ + /** + * Dispatches an event to all registered listeners. + * + * @param object $event The event to pass to the event handlers/listeners + * @param string|null $eventName The name of the event to dispatch. If not supplied, + * the class of $event should be used instead. + * + * @return object The passed $event MUST be returned + */ + public function dispatch(object $event, string $eventName = null): object; +} diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/LICENSE b/plugins/email/vendor/symfony/event-dispatcher-contracts/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/README.md b/plugins/email/vendor/symfony/event-dispatcher-contracts/README.md new file mode 100644 index 0000000..b1ab4c0 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/README.md @@ -0,0 +1,9 @@ +Symfony EventDispatcher Contracts +================================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/plugins/email/vendor/symfony/event-dispatcher-contracts/composer.json b/plugins/email/vendor/symfony/event-dispatcher-contracts/composer.json new file mode 100644 index 0000000..660df81 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher-contracts/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/event-dispatcher-contracts", + "type": "library", + "description": "Generic abstractions related to dispatching event", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php b/plugins/email/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php new file mode 100644 index 0000000..bb931b8 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Attribute; + +/** + * Service tag to autoconfigure event listeners. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class AsEventListener +{ + public function __construct( + public ?string $event = null, + public ?string $method = null, + public int $priority = 0, + public ?string $dispatcher = null, + ) { + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/CHANGELOG.md b/plugins/email/vendor/symfony/event-dispatcher/CHANGELOG.md new file mode 100644 index 0000000..0f98598 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/CHANGELOG.md @@ -0,0 +1,91 @@ +CHANGELOG +========= + +5.4 +--- + + * Allow `#[AsEventListener]` attribute on methods + +5.3 +--- + + * Add `#[AsEventListener]` attribute for declaring listeners on PHP 8 + +5.1.0 +----- + + * The `LegacyEventDispatcherProxy` class has been deprecated. + * Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`. + +5.0.0 +----- + + * The signature of the `EventDispatcherInterface::dispatch()` method has been changed to `dispatch($event, string $eventName = null): object`. + * The `Event` class has been removed in favor of `Symfony\Contracts\EventDispatcher\Event`. + * The `TraceableEventDispatcherInterface` has been removed. + * The `WrappedListener` class is now final. + +4.4.0 +----- + + * `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`. + * Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events. + +4.3.0 +----- + + * The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated + * deprecated the `Event` class, use `Symfony\Contracts\EventDispatcher\Event` instead + +4.1.0 +----- + + * added support for invokable event listeners tagged with `kernel.event_listener` by default + * The `TraceableEventDispatcher::getOrphanedEvents()` method has been added. + * The `TraceableEventDispatcherInterface` has been deprecated. + +4.0.0 +----- + + * removed the `ContainerAwareEventDispatcher` class + * added the `reset()` method to the `TraceableEventDispatcherInterface` + +3.4.0 +----- + + * Implementing `TraceableEventDispatcherInterface` without the `reset()` method has been deprecated. + +3.3.0 +----- + + * The ContainerAwareEventDispatcher class has been deprecated. Use EventDispatcher with closure factories instead. + +3.0.0 +----- + + * The method `getListenerPriority($eventName, $listener)` has been added to the + `EventDispatcherInterface`. + * The methods `Event::setDispatcher()`, `Event::getDispatcher()`, `Event::setName()` + and `Event::getName()` have been removed. + The event dispatcher and the event name are passed to the listener call. + +2.5.0 +----- + + * added Debug\TraceableEventDispatcher (originally in HttpKernel) + * changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface + * added RegisterListenersPass (originally in HttpKernel) + +2.1.0 +----- + + * added TraceableEventDispatcherInterface + * added ContainerAwareEventDispatcher + * added a reference to the EventDispatcher on the Event + * added a reference to the Event name on the event + * added fluid interface to the dispatch() method which now returns the Event + object + * added GenericEvent event class + * added the possibility for subscribers to subscribe several times for the + same event + * added ImmutableEventDispatcher diff --git a/plugins/email/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php b/plugins/email/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php new file mode 100644 index 0000000..acfbf61 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php @@ -0,0 +1,366 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Psr\EventDispatcher\StoppableEventInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Collects some data about event listeners. + * + * This event dispatcher delegates the dispatching to another one. + * + * @author Fabien Potencier + */ +class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface +{ + protected $logger; + protected $stopwatch; + + /** + * @var \SplObjectStorage + */ + private $callStack; + private $dispatcher; + private $wrappedListeners; + private $orphanedEvents; + private $requestStack; + private $currentRequestHash = ''; + + public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null, RequestStack $requestStack = null) + { + $this->dispatcher = $dispatcher; + $this->stopwatch = $stopwatch; + $this->logger = $logger; + $this->wrappedListeners = []; + $this->orphanedEvents = []; + $this->requestStack = $requestStack; + } + + /** + * {@inheritdoc} + */ + public function addListener(string $eventName, $listener, int $priority = 0) + { + $this->dispatcher->addListener($eventName, $listener, $priority); + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + $this->dispatcher->addSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function removeListener(string $eventName, $listener) + { + if (isset($this->wrappedListeners[$eventName])) { + foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { + $listener = $wrappedListener; + unset($this->wrappedListeners[$eventName][$index]); + break; + } + } + } + + return $this->dispatcher->removeListener($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + return $this->dispatcher->removeSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function getListeners(string $eventName = null) + { + return $this->dispatcher->getListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function getListenerPriority(string $eventName, $listener) + { + // we might have wrapped listeners for the event (if called while dispatching) + // in that case get the priority by wrapper + if (isset($this->wrappedListeners[$eventName])) { + foreach ($this->wrappedListeners[$eventName] as $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { + return $this->dispatcher->getListenerPriority($eventName, $wrappedListener); + } + } + } + + return $this->dispatcher->getListenerPriority($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function hasListeners(string $eventName = null) + { + return $this->dispatcher->hasListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $event, string $eventName = null): object + { + $eventName = $eventName ?? \get_class($event); + + if (null === $this->callStack) { + $this->callStack = new \SplObjectStorage(); + } + + $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; + + if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + $this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)); + } + + $this->preProcess($eventName); + try { + $this->beforeDispatch($eventName, $event); + try { + $e = $this->stopwatch->start($eventName, 'section'); + try { + $this->dispatcher->dispatch($event, $eventName); + } finally { + if ($e->isStarted()) { + $e->stop(); + } + } + } finally { + $this->afterDispatch($eventName, $event); + } + } finally { + $this->currentRequestHash = $currentRequestHash; + $this->postProcess($eventName); + } + + return $event; + } + + /** + * @return array + */ + public function getCalledListeners(Request $request = null) + { + if (null === $this->callStack) { + return []; + } + + $hash = $request ? spl_object_hash($request) : null; + $called = []; + foreach ($this->callStack as $listener) { + [$eventName, $requestHash] = $this->callStack->getInfo(); + if (null === $hash || $hash === $requestHash) { + $called[] = $listener->getInfo($eventName); + } + } + + return $called; + } + + /** + * @return array + */ + public function getNotCalledListeners(Request $request = null) + { + try { + $allListeners = $this->getListeners(); + } catch (\Exception $e) { + if (null !== $this->logger) { + $this->logger->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); + } + + // unable to retrieve the uncalled listeners + return []; + } + + $hash = $request ? spl_object_hash($request) : null; + $calledListeners = []; + + if (null !== $this->callStack) { + foreach ($this->callStack as $calledListener) { + [, $requestHash] = $this->callStack->getInfo(); + + if (null === $hash || $hash === $requestHash) { + $calledListeners[] = $calledListener->getWrappedListener(); + } + } + } + + $notCalled = []; + foreach ($allListeners as $eventName => $listeners) { + foreach ($listeners as $listener) { + if (!\in_array($listener, $calledListeners, true)) { + if (!$listener instanceof WrappedListener) { + $listener = new WrappedListener($listener, null, $this->stopwatch, $this); + } + $notCalled[] = $listener->getInfo($eventName); + } + } + } + + uasort($notCalled, [$this, 'sortNotCalledListeners']); + + return $notCalled; + } + + public function getOrphanedEvents(Request $request = null): array + { + if ($request) { + return $this->orphanedEvents[spl_object_hash($request)] ?? []; + } + + if (!$this->orphanedEvents) { + return []; + } + + return array_merge(...array_values($this->orphanedEvents)); + } + + public function reset() + { + $this->callStack = null; + $this->orphanedEvents = []; + $this->currentRequestHash = ''; + } + + /** + * Proxies all method calls to the original event dispatcher. + * + * @param string $method The method name + * @param array $arguments The method arguments + * + * @return mixed + */ + public function __call(string $method, array $arguments) + { + return $this->dispatcher->{$method}(...$arguments); + } + + /** + * Called before dispatching the event. + */ + protected function beforeDispatch(string $eventName, object $event) + { + } + + /** + * Called after dispatching the event. + */ + protected function afterDispatch(string $eventName, object $event) + { + } + + private function preProcess(string $eventName): void + { + if (!$this->dispatcher->hasListeners($eventName)) { + $this->orphanedEvents[$this->currentRequestHash][] = $eventName; + + return; + } + + foreach ($this->dispatcher->getListeners($eventName) as $listener) { + $priority = $this->getListenerPriority($eventName, $listener); + $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this); + $this->wrappedListeners[$eventName][] = $wrappedListener; + $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->addListener($eventName, $wrappedListener, $priority); + $this->callStack->attach($wrappedListener, [$eventName, $this->currentRequestHash]); + } + } + + private function postProcess(string $eventName): void + { + unset($this->wrappedListeners[$eventName]); + $skipped = false; + foreach ($this->dispatcher->getListeners($eventName) as $listener) { + if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch. + continue; + } + // Unwrap listener + $priority = $this->getListenerPriority($eventName, $listener); + $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority); + + if (null !== $this->logger) { + $context = ['event' => $eventName, 'listener' => $listener->getPretty()]; + } + + if ($listener->wasCalled()) { + if (null !== $this->logger) { + $this->logger->debug('Notified event "{event}" to listener "{listener}".', $context); + } + } else { + $this->callStack->detach($listener); + } + + if (null !== $this->logger && $skipped) { + $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context); + } + + if ($listener->stoppedPropagation()) { + if (null !== $this->logger) { + $this->logger->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); + } + + $skipped = true; + } + } + } + + private function sortNotCalledListeners(array $a, array $b) + { + if (0 !== $cmp = strcmp($a['event'], $b['event'])) { + return $cmp; + } + + if (\is_int($a['priority']) && !\is_int($b['priority'])) { + return 1; + } + + if (!\is_int($a['priority']) && \is_int($b['priority'])) { + return -1; + } + + if ($a['priority'] === $b['priority']) { + return 0; + } + + if ($a['priority'] > $b['priority']) { + return -1; + } + + return 1; + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/Debug/WrappedListener.php b/plugins/email/vendor/symfony/event-dispatcher/Debug/WrappedListener.php new file mode 100644 index 0000000..3916716 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/Debug/WrappedListener.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Psr\EventDispatcher\StoppableEventInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @author Fabien Potencier + */ +final class WrappedListener +{ + private $listener; + private $optimizedListener; + private $name; + private $called; + private $stoppedPropagation; + private $stopwatch; + private $dispatcher; + private $pretty; + private $stub; + private $priority; + private static $hasClassStub; + + public function __construct($listener, ?string $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null) + { + $this->listener = $listener; + $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? \Closure::fromCallable($listener) : null); + $this->stopwatch = $stopwatch; + $this->dispatcher = $dispatcher; + $this->called = false; + $this->stoppedPropagation = false; + + if (\is_array($listener)) { + $this->name = \is_object($listener[0]) ? get_debug_type($listener[0]) : $listener[0]; + $this->pretty = $this->name.'::'.$listener[1]; + } elseif ($listener instanceof \Closure) { + $r = new \ReflectionFunction($listener); + if (str_contains($r->name, '{closure}')) { + $this->pretty = $this->name = 'closure'; + } elseif ($class = $r->getClosureScopeClass()) { + $this->name = $class->name; + $this->pretty = $this->name.'::'.$r->name; + } else { + $this->pretty = $this->name = $r->name; + } + } elseif (\is_string($listener)) { + $this->pretty = $this->name = $listener; + } else { + $this->name = get_debug_type($listener); + $this->pretty = $this->name.'::__invoke'; + } + + if (null !== $name) { + $this->name = $name; + } + + if (null === self::$hasClassStub) { + self::$hasClassStub = class_exists(ClassStub::class); + } + } + + public function getWrappedListener() + { + return $this->listener; + } + + public function wasCalled(): bool + { + return $this->called; + } + + public function stoppedPropagation(): bool + { + return $this->stoppedPropagation; + } + + public function getPretty(): string + { + return $this->pretty; + } + + public function getInfo(string $eventName): array + { + if (null === $this->stub) { + $this->stub = self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->listener) : $this->pretty.'()'; + } + + return [ + 'event' => $eventName, + 'priority' => null !== $this->priority ? $this->priority : (null !== $this->dispatcher ? $this->dispatcher->getListenerPriority($eventName, $this->listener) : null), + 'pretty' => $this->pretty, + 'stub' => $this->stub, + ]; + } + + public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void + { + $dispatcher = $this->dispatcher ?: $dispatcher; + + $this->called = true; + $this->priority = $dispatcher->getListenerPriority($eventName, $this->listener); + + $e = $this->stopwatch->start($this->name, 'event_listener'); + + ($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher); + + if ($e->isStarted()) { + $e->stop(); + } + + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + $this->stoppedPropagation = true; + } + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php b/plugins/email/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php new file mode 100644 index 0000000..6e7292b --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * This pass allows bundles to extend the list of event aliases. + * + * @author Alexander M. Turek + */ +class AddEventAliasesPass implements CompilerPassInterface +{ + private $eventAliases; + private $eventAliasesParameter; + + public function __construct(array $eventAliases, string $eventAliasesParameter = 'event_dispatcher.event_aliases') + { + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->eventAliases = $eventAliases; + $this->eventAliasesParameter = $eventAliasesParameter; + } + + public function process(ContainerBuilder $container): void + { + $eventAliases = $container->hasParameter($this->eventAliasesParameter) ? $container->getParameter($this->eventAliasesParameter) : []; + + $container->setParameter( + $this->eventAliasesParameter, + array_merge($eventAliases, $this->eventAliases) + ); + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php b/plugins/email/vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php new file mode 100644 index 0000000..8eabe7d --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Compiler pass to register tagged services for an event dispatcher. + */ +class RegisterListenersPass implements CompilerPassInterface +{ + protected $dispatcherService; + protected $listenerTag; + protected $subscriberTag; + protected $eventAliasesParameter; + + private $hotPathEvents = []; + private $hotPathTagName = 'container.hot_path'; + private $noPreloadEvents = []; + private $noPreloadTagName = 'container.no_preload'; + + public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber', string $eventAliasesParameter = 'event_dispatcher.event_aliases') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->dispatcherService = $dispatcherService; + $this->listenerTag = $listenerTag; + $this->subscriberTag = $subscriberTag; + $this->eventAliasesParameter = $eventAliasesParameter; + } + + /** + * @return $this + */ + public function setHotPathEvents(array $hotPathEvents) + { + $this->hotPathEvents = array_flip($hotPathEvents); + + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__); + $this->hotPathTagName = func_get_arg(1); + } + + return $this; + } + + /** + * @return $this + */ + public function setNoPreloadEvents(array $noPreloadEvents): self + { + $this->noPreloadEvents = array_flip($noPreloadEvents); + + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__); + $this->noPreloadTagName = func_get_arg(1); + } + + return $this; + } + + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { + return; + } + + $aliases = []; + + if ($container->hasParameter($this->eventAliasesParameter)) { + $aliases = $container->getParameter($this->eventAliasesParameter); + } + + $globalDispatcherDefinition = $container->findDefinition($this->dispatcherService); + + foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) { + $noPreload = 0; + + foreach ($events as $event) { + $priority = $event['priority'] ?? 0; + + if (!isset($event['event'])) { + if ($container->getDefinition($id)->hasTag($this->subscriberTag)) { + continue; + } + + $event['method'] = $event['method'] ?? '__invoke'; + $event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']); + } + + $event['event'] = $aliases[$event['event']] ?? $event['event']; + + if (!isset($event['method'])) { + $event['method'] = 'on'.preg_replace_callback([ + '/(?<=\b|_)[a-z]/i', + '/[^a-z0-9]/i', + ], function ($matches) { return strtoupper($matches[0]); }, $event['event']); + $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); + + if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) { + $event['method'] = '__invoke'; + } + } + + $dispatcherDefinition = $globalDispatcherDefinition; + if (isset($event['dispatcher'])) { + $dispatcherDefinition = $container->getDefinition($event['dispatcher']); + } + + $dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); + + if (isset($this->hotPathEvents[$event['event']])) { + $container->getDefinition($id)->addTag($this->hotPathTagName); + } elseif (isset($this->noPreloadEvents[$event['event']])) { + ++$noPreload; + } + } + + if ($noPreload && \count($events) === $noPreload) { + $container->getDefinition($id)->addTag($this->noPreloadTagName); + } + } + + $extractingDispatcher = new ExtractingEventDispatcher(); + + foreach ($container->findTaggedServiceIds($this->subscriberTag, true) as $id => $tags) { + $def = $container->getDefinition($id); + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(EventSubscriberInterface::class)) { + throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class)); + } + $class = $r->name; + + $dispatcherDefinitions = []; + foreach ($tags as $attributes) { + if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) { + continue; + } + + $dispatcherDefinitions[$attributes['dispatcher']] = $container->getDefinition($attributes['dispatcher']); + } + + if (!$dispatcherDefinitions) { + $dispatcherDefinitions = [$globalDispatcherDefinition]; + } + + $noPreload = 0; + ExtractingEventDispatcher::$aliases = $aliases; + ExtractingEventDispatcher::$subscriber = $class; + $extractingDispatcher->addSubscriber($extractingDispatcher); + foreach ($extractingDispatcher->listeners as $args) { + $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]]; + foreach ($dispatcherDefinitions as $dispatcherDefinition) { + $dispatcherDefinition->addMethodCall('addListener', $args); + } + + if (isset($this->hotPathEvents[$args[0]])) { + $container->getDefinition($id)->addTag($this->hotPathTagName); + } elseif (isset($this->noPreloadEvents[$args[0]])) { + ++$noPreload; + } + } + if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) { + $container->getDefinition($id)->addTag($this->noPreloadTagName); + } + $extractingDispatcher->listeners = []; + ExtractingEventDispatcher::$aliases = []; + } + } + + private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string + { + if ( + null === ($class = $container->getDefinition($id)->getClass()) + || !($r = $container->getReflectionClass($class, false)) + || !$r->hasMethod($method) + || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() + || !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType + || $type->isBuiltin() + || Event::class === ($name = $type->getName()) + ) { + throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + } + + return $name; + } +} + +/** + * @internal + */ +class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface +{ + public $listeners = []; + + public static $aliases = []; + public static $subscriber; + + public function addListener(string $eventName, $listener, int $priority = 0) + { + $this->listeners[] = [$eventName, $listener[1], $priority]; + } + + public static function getSubscribedEvents(): array + { + $events = []; + + foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) { + $events[self::$aliases[$eventName] ?? $eventName] = $params; + } + + return $events; + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/EventDispatcher.php b/plugins/email/vendor/symfony/event-dispatcher/EventDispatcher.php new file mode 100644 index 0000000..8fe8fb5 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/EventDispatcher.php @@ -0,0 +1,280 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; +use Symfony\Component\EventDispatcher\Debug\WrappedListener; + +/** + * The EventDispatcherInterface is the central point of Symfony's event listener system. + * + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Fabien Potencier + * @author Jordi Boggiano + * @author Jordan Alliot + * @author Nicolas Grekas + */ +class EventDispatcher implements EventDispatcherInterface +{ + private $listeners = []; + private $sorted = []; + private $optimized; + + public function __construct() + { + if (__CLASS__ === static::class) { + $this->optimized = []; + } + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $event, string $eventName = null): object + { + $eventName = $eventName ?? \get_class($event); + + if (null !== $this->optimized) { + $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName)); + } else { + $listeners = $this->getListeners($eventName); + } + + if ($listeners) { + $this->callListeners($listeners, $eventName, $event); + } + + return $event; + } + + /** + * {@inheritdoc} + */ + public function getListeners(string $eventName = null) + { + if (null !== $eventName) { + if (empty($this->listeners[$eventName])) { + return []; + } + + if (!isset($this->sorted[$eventName])) { + $this->sortListeners($eventName); + } + + return $this->sorted[$eventName]; + } + + foreach ($this->listeners as $eventName => $eventListeners) { + if (!isset($this->sorted[$eventName])) { + $this->sortListeners($eventName); + } + } + + return array_filter($this->sorted); + } + + /** + * {@inheritdoc} + */ + public function getListenerPriority(string $eventName, $listener) + { + if (empty($this->listeners[$eventName])) { + return null; + } + + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + + foreach ($this->listeners[$eventName] as $priority => &$listeners) { + foreach ($listeners as &$v) { + if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { + $v[0] = $v[0](); + $v[1] = $v[1] ?? '__invoke'; + } + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { + return $priority; + } + } + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function hasListeners(string $eventName = null) + { + if (null !== $eventName) { + return !empty($this->listeners[$eventName]); + } + + foreach ($this->listeners as $eventListeners) { + if ($eventListeners) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addListener(string $eventName, $listener, int $priority = 0) + { + $this->listeners[$eventName][$priority][] = $listener; + unset($this->sorted[$eventName], $this->optimized[$eventName]); + } + + /** + * {@inheritdoc} + */ + public function removeListener(string $eventName, $listener) + { + if (empty($this->listeners[$eventName])) { + return; + } + + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + + foreach ($this->listeners[$eventName] as $priority => &$listeners) { + foreach ($listeners as $k => &$v) { + if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { + $v[0] = $v[0](); + $v[1] = $v[1] ?? '__invoke'; + } + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { + unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]); + } + } + + if (!$listeners) { + unset($this->listeners[$eventName][$priority]); + } + } + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_string($params)) { + $this->addListener($eventName, [$subscriber, $params]); + } elseif (\is_string($params[0])) { + $this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0); + } else { + foreach ($params as $listener) { + $this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_array($params) && \is_array($params[0])) { + foreach ($params as $listener) { + $this->removeListener($eventName, [$subscriber, $listener[0]]); + } + } else { + $this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]); + } + } + } + + /** + * Triggers the listeners of an event. + * + * This method can be overridden to add functionality that is executed + * for each listener. + * + * @param callable[] $listeners The event listeners + * @param string $eventName The name of the event to dispatch + * @param object $event The event object to pass to the event handlers/listeners + */ + protected function callListeners(iterable $listeners, string $eventName, object $event) + { + $stoppable = $event instanceof StoppableEventInterface; + + foreach ($listeners as $listener) { + if ($stoppable && $event->isPropagationStopped()) { + break; + } + $listener($event, $eventName, $this); + } + } + + /** + * Sorts the internal list of listeners for the given event by priority. + */ + private function sortListeners(string $eventName) + { + krsort($this->listeners[$eventName]); + $this->sorted[$eventName] = []; + + foreach ($this->listeners[$eventName] as &$listeners) { + foreach ($listeners as $k => &$listener) { + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + $this->sorted[$eventName][] = $listener; + } + } + } + + /** + * Optimizes the internal list of listeners for the given event by priority. + */ + private function optimizeListeners(string $eventName): array + { + krsort($this->listeners[$eventName]); + $this->optimized[$eventName] = []; + + foreach ($this->listeners[$eventName] as &$listeners) { + foreach ($listeners as &$listener) { + $closure = &$this->optimized[$eventName][]; + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $closure = static function (...$args) use (&$listener, &$closure) { + if ($listener[0] instanceof \Closure) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + ($closure = \Closure::fromCallable($listener))(...$args); + }; + } else { + $closure = $listener instanceof \Closure || $listener instanceof WrappedListener ? $listener : \Closure::fromCallable($listener); + } + } + } + + return $this->optimized[$eventName]; + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/EventDispatcherInterface.php b/plugins/email/vendor/symfony/event-dispatcher/EventDispatcherInterface.php new file mode 100644 index 0000000..cc324e1 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/EventDispatcherInterface.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; + +/** + * The EventDispatcherInterface is the central point of Symfony's event listener system. + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @author Bernhard Schussek + */ +interface EventDispatcherInterface extends ContractsEventDispatcherInterface +{ + /** + * Adds an event listener that listens on the specified events. + * + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function addListener(string $eventName, callable $listener, int $priority = 0); + + /** + * Adds an event subscriber. + * + * The subscriber is asked for all the events it is + * interested in and added as a listener for these events. + */ + public function addSubscriber(EventSubscriberInterface $subscriber); + + /** + * Removes an event listener from the specified events. + */ + public function removeListener(string $eventName, callable $listener); + + public function removeSubscriber(EventSubscriberInterface $subscriber); + + /** + * Gets the listeners of a specific event or all listeners sorted by descending priority. + * + * @return array + */ + public function getListeners(string $eventName = null); + + /** + * Gets the listener priority for a specific event. + * + * Returns null if the event or the listener does not exist. + * + * @return int|null + */ + public function getListenerPriority(string $eventName, callable $listener); + + /** + * Checks whether an event has any registered listeners. + * + * @return bool + */ + public function hasListeners(string $eventName = null); +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/EventSubscriberInterface.php b/plugins/email/vendor/symfony/event-dispatcher/EventSubscriberInterface.php new file mode 100644 index 0000000..2085e42 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/EventSubscriberInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +/** + * An EventSubscriber knows itself what events it is interested in. + * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes + * {@link getSubscribedEvents} and registers the subscriber as a listener for all + * returned events. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + */ +interface EventSubscriberInterface +{ + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * ['eventName' => 'methodName'] + * * ['eventName' => ['methodName', $priority]] + * * ['eventName' => [['methodName1', $priority], ['methodName2']]] + * + * The code must not depend on runtime state as it will only be called at compile time. + * All logic depending on runtime state must be put into the individual methods handling the events. + * + * @return array> + */ + public static function getSubscribedEvents(); +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/GenericEvent.php b/plugins/email/vendor/symfony/event-dispatcher/GenericEvent.php new file mode 100644 index 0000000..b32a301 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/GenericEvent.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Event encapsulation class. + * + * Encapsulates events thus decoupling the observer from the subject they encapsulate. + * + * @author Drak + * + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate +{ + protected $subject; + protected $arguments; + + /** + * Encapsulate an event with $subject and $args. + * + * @param mixed $subject The subject of the event, usually an object or a callable + * @param array $arguments Arguments to store in the event + */ + public function __construct($subject = null, array $arguments = []) + { + $this->subject = $subject; + $this->arguments = $arguments; + } + + /** + * Getter for subject property. + * + * @return mixed + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Get argument by key. + * + * @return mixed + * + * @throws \InvalidArgumentException if key is not found + */ + public function getArgument(string $key) + { + if ($this->hasArgument($key)) { + return $this->arguments[$key]; + } + + throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key)); + } + + /** + * Add argument to event. + * + * @param mixed $value Value + * + * @return $this + */ + public function setArgument(string $key, $value) + { + $this->arguments[$key] = $value; + + return $this; + } + + /** + * Getter for all arguments. + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Set args property. + * + * @return $this + */ + public function setArguments(array $args = []) + { + $this->arguments = $args; + + return $this; + } + + /** + * Has argument. + * + * @return bool + */ + public function hasArgument(string $key) + { + return \array_key_exists($key, $this->arguments); + } + + /** + * ArrayAccess for argument getter. + * + * @param string $key Array key + * + * @return mixed + * + * @throws \InvalidArgumentException if key does not exist in $this->args + */ + #[\ReturnTypeWillChange] + public function offsetGet($key) + { + return $this->getArgument($key); + } + + /** + * ArrayAccess for argument setter. + * + * @param string $key Array key to set + * @param mixed $value Value + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($key, $value) + { + $this->setArgument($key, $value); + } + + /** + * ArrayAccess for unset argument. + * + * @param string $key Array key + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($key) + { + if ($this->hasArgument($key)) { + unset($this->arguments[$key]); + } + } + + /** + * ArrayAccess has argument. + * + * @param string $key Array key + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($key) + { + return $this->hasArgument($key); + } + + /** + * IteratorAggregate for iterating over the object like an array. + * + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->arguments); + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php b/plugins/email/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php new file mode 100644 index 0000000..568d79c --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +/** + * A read-only proxy for an event dispatcher. + * + * @author Bernhard Schussek + */ +class ImmutableEventDispatcher implements EventDispatcherInterface +{ + private $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher) + { + $this->dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $event, string $eventName = null): object + { + return $this->dispatcher->dispatch($event, $eventName); + } + + /** + * {@inheritdoc} + */ + public function addListener(string $eventName, $listener, int $priority = 0) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function removeListener(string $eventName, $listener) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function getListeners(string $eventName = null) + { + return $this->dispatcher->getListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function getListenerPriority(string $eventName, $listener) + { + return $this->dispatcher->getListenerPriority($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function hasListeners(string $eventName = null) + { + return $this->dispatcher->hasListeners($eventName); + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/LICENSE b/plugins/email/vendor/symfony/event-dispatcher/LICENSE new file mode 100644 index 0000000..88bf75b --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php b/plugins/email/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php new file mode 100644 index 0000000..6e17c8f --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +trigger_deprecation('symfony/event-dispatcher', '5.1', '%s is deprecated, use the event dispatcher without the proxy.', LegacyEventDispatcherProxy::class); + +/** + * A helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch(). + * + * @author Nicolas Grekas + * + * @deprecated since Symfony 5.1 + */ +final class LegacyEventDispatcherProxy +{ + public static function decorate(?EventDispatcherInterface $dispatcher): ?EventDispatcherInterface + { + return $dispatcher; + } +} diff --git a/plugins/email/vendor/symfony/event-dispatcher/README.md b/plugins/email/vendor/symfony/event-dispatcher/README.md new file mode 100644 index 0000000..dcdb68d --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/README.md @@ -0,0 +1,15 @@ +EventDispatcher Component +========================= + +The EventDispatcher component provides tools that allow your application +components to communicate with each other by dispatching events and listening to +them. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/email/vendor/symfony/event-dispatcher/composer.json b/plugins/email/vendor/symfony/event-dispatcher/composer.json new file mode 100644 index 0000000..32b42e4 --- /dev/null +++ b/plugins/email/vendor/symfony/event-dispatcher/composer.json @@ -0,0 +1,52 @@ +{ + "name": "symfony/event-dispatcher", + "type": "library", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/mailer/CHANGELOG.md b/plugins/email/vendor/symfony/mailer/CHANGELOG.md new file mode 100644 index 0000000..cc6cd6f --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/CHANGELOG.md @@ -0,0 +1,61 @@ +CHANGELOG +========= + +5.4 +--- + + * Enable the mailer to operate on any PSR-14-compatible event dispatcher + +5.3 +--- + + * added the `mailer` monolog channel and set it on all transport definitions + +5.2.0 +----- + + * added `NativeTransportFactory` to configure a transport based on php.ini settings + * added `local_domain`, `restart_threshold`, `restart_threshold_sleep` and `ping_threshold` options for `smtp` + * added `command` option for `sendmail` + +4.4.0 +----- + + * [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null` + * [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to + `DelayedEnvelope` + * [BC BREAK] changed the syntax for failover and roundrobin DSNs + + Before: + + dummy://a || dummy://b (for failover) + dummy://a && dummy://b (for roundrobin) + + After: + + failover(dummy://a dummy://b) + roundrobin(dummy://a dummy://b) + + * added support for multiple transports on a `Mailer` instance + * [BC BREAK] removed the `auth_mode` DSN option (it is now always determined automatically) + * STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS) + * [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead) + * Added support for the `smtps` protocol (does the same as using `smtp` and port `465`) + * Added PHPUnit constraints + * Added `MessageDataCollector` + * Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails + * [BC BREAK] `TransportInterface` has a new `__toString()` method + * [BC BREAK] Classes `AbstractApiTransport` and `AbstractHttpTransport` moved under `Transport` sub-namespace. + * [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` + instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`. + * Added possibility to register custom transport for dsn by implementing + `Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI. + * Added `Symfony\Component\Mailer\Test\TransportFactoryTestCase` to ease testing custom transport factories. + * Added `SentMessage::getDebug()` and `TransportExceptionInterface::getDebug` to help debugging + * Made `MessageEvent` final + * add DSN parameter `verify_peer` to disable TLS peer verification for SMTP transport + +4.3.0 +----- + + * Added the component. diff --git a/plugins/email/vendor/symfony/mailer/DataCollector/MessageDataCollector.php b/plugins/email/vendor/symfony/mailer/DataCollector/MessageDataCollector.php new file mode 100644 index 0000000..07f77b2 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/DataCollector/MessageDataCollector.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\Mailer\Event\MessageEvents; +use Symfony\Component\Mailer\EventListener\MessageLoggerListener; + +/** + * @author Fabien Potencier + */ +final class MessageDataCollector extends DataCollector +{ + private $events; + + public function __construct(MessageLoggerListener $logger) + { + $this->events = $logger->getEvents(); + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + $this->data['events'] = $this->events; + } + + public function getEvents(): MessageEvents + { + return $this->data['events']; + } + + /** + * @internal + */ + public function base64Encode(string $data): string + { + return base64_encode($data); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->data = []; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'mailer'; + } +} diff --git a/plugins/email/vendor/symfony/mailer/DelayedEnvelope.php b/plugins/email/vendor/symfony/mailer/DelayedEnvelope.php new file mode 100644 index 0000000..b78b6f2 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/DelayedEnvelope.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Message; + +/** + * @author Fabien Potencier + * + * @internal + */ +final class DelayedEnvelope extends Envelope +{ + private $senderSet = false; + private $recipientsSet = false; + private $message; + + public function __construct(Message $message) + { + $this->message = $message; + } + + public function setSender(Address $sender): void + { + parent::setSender($sender); + + $this->senderSet = true; + } + + public function getSender(): Address + { + if (!$this->senderSet) { + parent::setSender(self::getSenderFromHeaders($this->message->getHeaders())); + } + + return parent::getSender(); + } + + public function setRecipients(array $recipients): void + { + parent::setRecipients($recipients); + + $this->recipientsSet = parent::getRecipients(); + } + + /** + * @return Address[] + */ + public function getRecipients(): array + { + if ($this->recipientsSet) { + return parent::getRecipients(); + } + + return self::getRecipientsFromHeaders($this->message->getHeaders()); + } + + private static function getRecipientsFromHeaders(Headers $headers): array + { + $recipients = []; + foreach (['to', 'cc', 'bcc'] as $name) { + foreach ($headers->all($name) as $header) { + foreach ($header->getAddresses() as $address) { + $recipients[] = $address; + } + } + } + + return $recipients; + } + + private static function getSenderFromHeaders(Headers $headers): Address + { + if ($sender = $headers->get('Sender')) { + return $sender->getAddress(); + } + if ($return = $headers->get('Return-Path')) { + return $return->getAddress(); + } + if ($from = $headers->get('From')) { + return $from->getAddresses()[0]; + } + + throw new LogicException('Unable to determine the sender of the message.'); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Envelope.php b/plugins/email/vendor/symfony/mailer/Envelope.php new file mode 100644 index 0000000..97c8d85 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Envelope.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class Envelope +{ + private $sender; + private $recipients = []; + + /** + * @param Address[] $recipients + */ + public function __construct(Address $sender, array $recipients) + { + $this->setSender($sender); + $this->setRecipients($recipients); + } + + public static function create(RawMessage $message): self + { + if (RawMessage::class === \get_class($message)) { + throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.'); + } + + return new DelayedEnvelope($message); + } + + public function setSender(Address $sender): void + { + // to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers + if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) { + throw new InvalidArgumentException(sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress())); + } + $this->sender = $sender; + } + + /** + * @return Address Returns a "mailbox" as specified by RFC 2822 + * Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress()) + */ + public function getSender(): Address + { + return $this->sender; + } + + /** + * @param Address[] $recipients + */ + public function setRecipients(array $recipients): void + { + if (!$recipients) { + throw new InvalidArgumentException('An envelope must have at least one recipient.'); + } + + $this->recipients = []; + foreach ($recipients as $recipient) { + if (!$recipient instanceof Address) { + throw new InvalidArgumentException(sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient))); + } + $this->recipients[] = new Address($recipient->getAddress()); + } + } + + /** + * @return Address[] + */ + public function getRecipients(): array + { + return $this->recipients; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Event/MessageEvent.php b/plugins/email/vendor/symfony/mailer/Event/MessageEvent.php new file mode 100644 index 0000000..d6b7894 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Event/MessageEvent.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Allows the transformation of a Message and the Envelope before the email is sent. + * + * @author Fabien Potencier + */ +final class MessageEvent extends Event +{ + private $message; + private $envelope; + private $transport; + private $queued; + + public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false) + { + $this->message = $message; + $this->envelope = $envelope; + $this->transport = $transport; + $this->queued = $queued; + } + + public function getMessage(): RawMessage + { + return $this->message; + } + + public function setMessage(RawMessage $message): void + { + $this->message = $message; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + public function setEnvelope(Envelope $envelope): void + { + $this->envelope = $envelope; + } + + public function getTransport(): string + { + return $this->transport; + } + + public function isQueued(): bool + { + return $this->queued; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Event/MessageEvents.php b/plugins/email/vendor/symfony/mailer/Event/MessageEvents.php new file mode 100644 index 0000000..b526649 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Event/MessageEvents.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class MessageEvents +{ + private $events = []; + private $transports = []; + + public function add(MessageEvent $event): void + { + $this->events[] = $event; + $this->transports[$event->getTransport()] = true; + } + + public function getTransports(): array + { + return array_keys($this->transports); + } + + /** + * @return MessageEvent[] + */ + public function getEvents(string $name = null): array + { + if (null === $name) { + return $this->events; + } + + $events = []; + foreach ($this->events as $event) { + if ($name === $event->getTransport()) { + $events[] = $event; + } + } + + return $events; + } + + /** + * @return RawMessage[] + */ + public function getMessages(string $name = null): array + { + $events = $this->getEvents($name); + $messages = []; + foreach ($events as $event) { + $messages[] = $event->getMessage(); + } + + return $messages; + } +} diff --git a/plugins/email/vendor/symfony/mailer/EventListener/EnvelopeListener.php b/plugins/email/vendor/symfony/mailer/EventListener/EnvelopeListener.php new file mode 100644 index 0000000..b2980bc --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/EventListener/EnvelopeListener.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Message; + +/** + * Manipulates the Envelope of a Message. + * + * @author Fabien Potencier + */ +class EnvelopeListener implements EventSubscriberInterface +{ + private $sender; + private $recipients; + + /** + * @param Address|string $sender + * @param array $recipients + */ + public function __construct($sender = null, array $recipients = null) + { + if (null !== $sender) { + $this->sender = Address::create($sender); + } + if (null !== $recipients) { + $this->recipients = Address::createArray($recipients); + } + } + + public function onMessage(MessageEvent $event): void + { + if ($this->sender) { + $event->getEnvelope()->setSender($this->sender); + + $message = $event->getMessage(); + if ($message instanceof Message) { + if (!$message->getHeaders()->has('Sender') && !$message->getHeaders()->has('From')) { + $message->getHeaders()->addMailboxHeader('Sender', $this->sender); + } + } + } + + if ($this->recipients) { + $event->getEnvelope()->setRecipients($this->recipients); + } + } + + public static function getSubscribedEvents() + { + return [ + // should be the last one to allow header changes by other listeners first + MessageEvent::class => ['onMessage', -255], + ]; + } +} diff --git a/plugins/email/vendor/symfony/mailer/EventListener/MessageListener.php b/plugins/email/vendor/symfony/mailer/EventListener/MessageListener.php new file mode 100644 index 0000000..f23c69d --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/EventListener/MessageListener.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\RuntimeException; +use Symfony\Component\Mime\BodyRendererInterface; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Header\MailboxListHeader; +use Symfony\Component\Mime\Message; + +/** + * Manipulates the headers and the body of a Message. + * + * @author Fabien Potencier + */ +class MessageListener implements EventSubscriberInterface +{ + public const HEADER_SET_IF_EMPTY = 1; + public const HEADER_ADD = 2; + public const HEADER_REPLACE = 3; + public const DEFAULT_RULES = [ + 'from' => self::HEADER_SET_IF_EMPTY, + 'return-path' => self::HEADER_SET_IF_EMPTY, + 'reply-to' => self::HEADER_ADD, + 'to' => self::HEADER_SET_IF_EMPTY, + 'cc' => self::HEADER_ADD, + 'bcc' => self::HEADER_ADD, + ]; + + private $headers; + private $headerRules = []; + private $renderer; + + public function __construct(Headers $headers = null, BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) + { + $this->headers = $headers; + $this->renderer = $renderer; + foreach ($headerRules as $headerName => $rule) { + $this->addHeaderRule($headerName, $rule); + } + } + + public function addHeaderRule(string $headerName, int $rule): void + { + if ($rule < 1 || $rule > 3) { + throw new InvalidArgumentException(sprintf('The "%d" rule is not supported.', $rule)); + } + + $this->headerRules[strtolower($headerName)] = $rule; + } + + public function onMessage(MessageEvent $event): void + { + $message = $event->getMessage(); + if (!$message instanceof Message) { + return; + } + + $this->setHeaders($message); + $this->renderMessage($message); + } + + private function setHeaders(Message $message): void + { + if (!$this->headers) { + return; + } + + $headers = $message->getHeaders(); + foreach ($this->headers->all() as $name => $header) { + if (!$headers->has($name)) { + $headers->add($header); + + continue; + } + + switch ($this->headerRules[$name] ?? self::HEADER_SET_IF_EMPTY) { + case self::HEADER_SET_IF_EMPTY: + break; + + case self::HEADER_REPLACE: + $headers->remove($name); + $headers->add($header); + + break; + + case self::HEADER_ADD: + if (!Headers::isUniqueHeader($name)) { + $headers->add($header); + + break; + } + + $h = $headers->get($name); + if (!$h instanceof MailboxListHeader) { + throw new RuntimeException(sprintf('Unable to set header "%s".', $name)); + } + + Headers::checkHeaderClass($header); + foreach ($header->getAddresses() as $address) { + $h->addAddress($address); + } + } + } + } + + private function renderMessage(Message $message): void + { + if (!$this->renderer) { + return; + } + + $this->renderer->render($message); + } + + public static function getSubscribedEvents() + { + return [ + MessageEvent::class => 'onMessage', + ]; + } +} diff --git a/plugins/email/vendor/symfony/mailer/EventListener/MessageLoggerListener.php b/plugins/email/vendor/symfony/mailer/EventListener/MessageLoggerListener.php new file mode 100644 index 0000000..093bf2b --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/EventListener/MessageLoggerListener.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\MessageEvents; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Logs Messages. + * + * @author Fabien Potencier + */ +class MessageLoggerListener implements EventSubscriberInterface, ResetInterface +{ + private $events; + + public function __construct() + { + $this->events = new MessageEvents(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->events = new MessageEvents(); + } + + public function onMessage(MessageEvent $event): void + { + $this->events->add($event); + } + + public function getEvents(): MessageEvents + { + return $this->events; + } + + public static function getSubscribedEvents() + { + return [ + MessageEvent::class => ['onMessage', -255], + ]; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/ExceptionInterface.php b/plugins/email/vendor/symfony/mailer/Exception/ExceptionInterface.php new file mode 100644 index 0000000..2f0f3a6 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Fabien Potencier + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/HttpTransportException.php b/plugins/email/vendor/symfony/mailer/Exception/HttpTransportException.php new file mode 100644 index 0000000..c72eb6c --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/HttpTransportException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +class HttpTransportException extends TransportException +{ + private $response; + + public function __construct(?string $message, ResponseInterface $response, int $code = 0, \Throwable $previous = null) + { + if (null === $message) { + trigger_deprecation('symfony/mailer', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/IncompleteDsnException.php b/plugins/email/vendor/symfony/mailer/Exception/IncompleteDsnException.php new file mode 100644 index 0000000..f2618b6 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/IncompleteDsnException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Konstantin Myakshin + */ +class IncompleteDsnException extends InvalidArgumentException +{ +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/InvalidArgumentException.php b/plugins/email/vendor/symfony/mailer/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..ba53334 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/LogicException.php b/plugins/email/vendor/symfony/mailer/Exception/LogicException.php new file mode 100644 index 0000000..487c0a3 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/RuntimeException.php b/plugins/email/vendor/symfony/mailer/Exception/RuntimeException.php new file mode 100644 index 0000000..44b79cc --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/TransportException.php b/plugins/email/vendor/symfony/mailer/Exception/TransportException.php new file mode 100644 index 0000000..dfad0c4 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/TransportException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class TransportException extends RuntimeException implements TransportExceptionInterface +{ + private $debug = ''; + + public function getDebug(): string + { + return $this->debug; + } + + public function appendDebug(string $debug): void + { + $this->debug .= $debug; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/TransportExceptionInterface.php b/plugins/email/vendor/symfony/mailer/Exception/TransportExceptionInterface.php new file mode 100644 index 0000000..4318f5c --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/TransportExceptionInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +interface TransportExceptionInterface extends ExceptionInterface +{ + public function getDebug(): string; + + public function appendDebug(string $debug): void; +} diff --git a/plugins/email/vendor/symfony/mailer/Exception/UnsupportedSchemeException.php b/plugins/email/vendor/symfony/mailer/Exception/UnsupportedSchemeException.php new file mode 100644 index 0000000..e47a129 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Exception/UnsupportedSchemeException.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +use Symfony\Component\Mailer\Bridge; +use Symfony\Component\Mailer\Transport\Dsn; + +/** + * @author Konstantin Myakshin + */ +class UnsupportedSchemeException extends LogicException +{ + private const SCHEME_TO_PACKAGE_MAP = [ + 'gmail' => [ + 'class' => Bridge\Google\Transport\GmailTransportFactory::class, + 'package' => 'symfony/google-mailer', + ], + 'mailgun' => [ + 'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class, + 'package' => 'symfony/mailgun-mailer', + ], + 'mailjet' => [ + 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, + 'package' => 'symfony/mailjet-mailer', + ], + 'mandrill' => [ + 'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class, + 'package' => 'symfony/mailchimp-mailer', + ], + 'ohmysmtp' => [ + 'class' => Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory::class, + 'package' => 'symfony/oh-my-smtp-mailer', + ], + 'postmark' => [ + 'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class, + 'package' => 'symfony/postmark-mailer', + ], + 'sendgrid' => [ + 'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class, + 'package' => 'symfony/sendgrid-mailer', + ], + 'sendinblue' => [ + 'class' => Bridge\Sendinblue\Transport\SendinblueTransportFactory::class, + 'package' => 'symfony/sendinblue-mailer', + ], + 'ses' => [ + 'class' => Bridge\Amazon\Transport\SesTransportFactory::class, + 'package' => 'symfony/amazon-mailer', + ], + ]; + + public function __construct(Dsn $dsn, string $name = null, array $supported = []) + { + $provider = $dsn->getScheme(); + if (false !== $pos = strpos($provider, '+')) { + $provider = substr($provider, 0, $pos); + } + $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed; try running "composer require %s".', $provider, $package['package'])); + + return; + } + + $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); + if ($name && $supported) { + $message .= sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported)); + } + + parent::__construct($message.'.'); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Header/MetadataHeader.php b/plugins/email/vendor/symfony/mailer/Header/MetadataHeader.php new file mode 100644 index 0000000..d56acb1 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Header/MetadataHeader.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Header; + +use Symfony\Component\Mime\Header\UnstructuredHeader; + +/** + * @author Kevin Bond + */ +final class MetadataHeader extends UnstructuredHeader +{ + private $key; + + public function __construct(string $key, string $value) + { + $this->key = $key; + + parent::__construct('X-Metadata-'.$key, $value); + } + + public function getKey(): string + { + return $this->key; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Header/TagHeader.php b/plugins/email/vendor/symfony/mailer/Header/TagHeader.php new file mode 100644 index 0000000..7115cae --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Header/TagHeader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Header; + +use Symfony\Component\Mime\Header\UnstructuredHeader; + +/** + * @author Kevin Bond + */ +final class TagHeader extends UnstructuredHeader +{ + public function __construct(string $value) + { + parent::__construct('X-Tag', $value); + } +} diff --git a/plugins/email/vendor/symfony/mailer/LICENSE b/plugins/email/vendor/symfony/mailer/LICENSE new file mode 100644 index 0000000..9c907a4 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/mailer/Mailer.php b/plugins/email/vendor/symfony/mailer/Mailer.php new file mode 100644 index 0000000..cbdcdf2 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Mailer.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Messenger\SendEmailMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface; + +/** + * @author Fabien Potencier + */ +final class Mailer implements MailerInterface +{ + private $transport; + private $bus; + private $dispatcher; + + public function __construct(TransportInterface $transport, MessageBusInterface $bus = null, EventDispatcherInterface $dispatcher = null) + { + $this->transport = $transport; + $this->bus = $bus; + $this->dispatcher = class_exists(Event::class) && $dispatcher instanceof SymfonyEventDispatcherInterface ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher; + } + + public function send(RawMessage $message, Envelope $envelope = null): void + { + if (null === $this->bus) { + $this->transport->send($message, $envelope); + + return; + } + + if (null !== $this->dispatcher) { + // The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let + // listeners do something before a message is sent to the queue. + // We are using a cloned message as we still want to dispatch the **original** message, not the one modified by listeners. + // That's because the listeners will run again when the email is sent via Messenger by the transport (see `AbstractTransport`). + // Listeners should act depending on the `$queued` argument of the `MessageEvent` instance. + $clonedMessage = clone $message; + $clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage); + $event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true); + $this->dispatcher->dispatch($event); + } + + try { + $this->bus->dispatch(new SendEmailMessage($message, $envelope)); + } catch (HandlerFailedException $e) { + foreach ($e->getNestedExceptions() as $nested) { + if ($nested instanceof TransportExceptionInterface) { + throw $nested; + } + } + throw $e; + } + } +} diff --git a/plugins/email/vendor/symfony/mailer/MailerInterface.php b/plugins/email/vendor/symfony/mailer/MailerInterface.php new file mode 100644 index 0000000..eb44cf6 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/MailerInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mime\RawMessage; + +/** + * Interface for mailers able to send emails synchronous and/or asynchronous. + * + * Implementations must support synchronous and asynchronous sending. + * + * @author Fabien Potencier + */ +interface MailerInterface +{ + /** + * @throws TransportExceptionInterface + */ + public function send(RawMessage $message, Envelope $envelope = null): void; +} diff --git a/plugins/email/vendor/symfony/mailer/Messenger/MessageHandler.php b/plugins/email/vendor/symfony/mailer/Messenger/MessageHandler.php new file mode 100644 index 0000000..fefae9d --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Messenger/MessageHandler.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Messenger; + +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Fabien Potencier + */ +class MessageHandler +{ + private $transport; + + public function __construct(TransportInterface $transport) + { + $this->transport = $transport; + } + + public function __invoke(SendEmailMessage $message): ?SentMessage + { + return $this->transport->send($message->getMessage(), $message->getEnvelope()); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Messenger/SendEmailMessage.php b/plugins/email/vendor/symfony/mailer/Messenger/SendEmailMessage.php new file mode 100644 index 0000000..b06ac83 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Messenger/SendEmailMessage.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Messenger; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class SendEmailMessage +{ + private $message; + private $envelope; + + public function __construct(RawMessage $message, Envelope $envelope = null) + { + $this->message = $message; + $this->envelope = $envelope; + } + + public function getMessage(): RawMessage + { + return $this->message; + } + + public function getEnvelope(): ?Envelope + { + return $this->envelope; + } +} diff --git a/plugins/email/vendor/symfony/mailer/README.md b/plugins/email/vendor/symfony/mailer/README.md new file mode 100644 index 0000000..93a9585 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/README.md @@ -0,0 +1,74 @@ +Mailer Component +================ + +The Mailer component helps sending emails. + +Getting Started +--------------- + +``` +$ composer require symfony/mailer +``` + +```php +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mime\Email; + +$transport = Transport::fromDsn('smtp://localhost'); +$mailer = new Mailer($transport); + +$email = (new Email()) + ->from('hello@example.com') + ->to('you@example.com') + //->cc('cc@example.com') + //->bcc('bcc@example.com') + //->replyTo('fabien@example.com') + //->priority(Email::PRIORITY_HIGH) + ->subject('Time for Symfony Mailer!') + ->text('Sending emails is fun again!') + ->html('

    See Twig integration for better HTML integration!

    '); + +$mailer->send($email); +``` + +To enable the Twig integration of the Mailer, require `symfony/twig-bridge` and +set up the `BodyRenderer`: + +```php +use Symfony\Bridge\Twig\Mime\BodyRenderer; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Mailer\EventListener\MessageListener; +use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\Transport; +use Twig\Environment as TwigEnvironment; + +$twig = new TwigEnvironment(...); +$messageListener = new MessageListener(null, new BodyRenderer($twig)); + +$eventDispatcher = new EventDispatcher(); +$eventDispatcher->addSubscriber($messageListener); + +$transport = Transport::fromDsn('smtp://localhost', $eventDispatcher); +$mailer = new Mailer($transport, null, $eventDispatcher); + +$email = (new TemplatedEmail()) + // ... + ->htmlTemplate('emails/signup.html.twig') + ->context([ + 'expiration_date' => new \DateTime('+7 days'), + 'username' => 'foo', + ]) +; +$mailer->send($email); +``` + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/mailer.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/email/vendor/symfony/mailer/SentMessage.php b/plugins/email/vendor/symfony/mailer/SentMessage.php new file mode 100644 index 0000000..1a12d07 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/SentMessage.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class SentMessage +{ + private $original; + private $raw; + private $envelope; + private $messageId; + private $debug = ''; + + /** + * @internal + */ + public function __construct(RawMessage $message, Envelope $envelope) + { + $message->ensureValidity(); + + $this->original = $message; + $this->envelope = $envelope; + + if ($message instanceof Message) { + $message = clone $message; + $headers = $message->getHeaders(); + if (!$headers->has('Message-ID')) { + $headers->addIdHeader('Message-ID', $message->generateMessageId()); + } + $this->messageId = $headers->get('Message-ID')->getId(); + $this->raw = new RawMessage($message->toIterable()); + } else { + $this->raw = $message; + } + } + + public function getMessage(): RawMessage + { + return $this->raw; + } + + public function getOriginalMessage(): RawMessage + { + return $this->original; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + public function setMessageId(string $id): void + { + $this->messageId = $id; + } + + public function getMessageId(): string + { + return $this->messageId; + } + + public function getDebug(): string + { + return $this->debug; + } + + public function appendDebug(string $debug): void + { + $this->debug .= $debug; + } + + public function toString(): string + { + return $this->raw->toString(); + } + + public function toIterable(): iterable + { + return $this->raw->toIterable(); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Test/Constraint/EmailCount.php b/plugins/email/vendor/symfony/mailer/Test/Constraint/EmailCount.php new file mode 100644 index 0000000..59a7812 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Test/Constraint/EmailCount.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mailer\Event\MessageEvents; + +final class EmailCount extends Constraint +{ + private $expectedValue; + private $transport; + private $queued; + + public function __construct(int $expectedValue, string $transport = null, bool $queued = false) + { + $this->expectedValue = $expectedValue; + $this->transport = $transport; + $this->queued = $queued; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue); + } + + /** + * @param MessageEvents $events + * + * {@inheritdoc} + */ + protected function matches($events): bool + { + return $this->expectedValue === $this->countEmails($events); + } + + /** + * @param MessageEvents $events + * + * {@inheritdoc} + */ + protected function failureDescription($events): string + { + return sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent'); + } + + private function countEmails(MessageEvents $events): int + { + $count = 0; + foreach ($events->getEvents($this->transport) as $event) { + if ( + ($this->queued && $event->isQueued()) + || + (!$this->queued && !$event->isQueued()) + ) { + ++$count; + } + } + + return $count; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Test/Constraint/EmailIsQueued.php b/plugins/email/vendor/symfony/mailer/Test/Constraint/EmailIsQueued.php new file mode 100644 index 0000000..be9dac8 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Test/Constraint/EmailIsQueued.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mailer\Event\MessageEvent; + +final class EmailIsQueued extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is queued'; + } + + /** + * @param MessageEvent $event + * + * {@inheritdoc} + */ + protected function matches($event): bool + { + return $event->isQueued(); + } + + /** + * @param MessageEvent $event + * + * {@inheritdoc} + */ + protected function failureDescription($event): string + { + return 'the Email '.$this->toString(); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Test/TransportFactoryTestCase.php b/plugins/email/vendor/symfony/mailer/Test/TransportFactoryTestCase.php new file mode 100644 index 0000000..c2426a0 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Test/TransportFactoryTestCase.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Test; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * A test case to ease testing Transport Factory. + * + * @author Konstantin Myakshin + */ +abstract class TransportFactoryTestCase extends TestCase +{ + protected const USER = 'u$er'; + protected const PASSWORD = 'pa$s'; + + protected $dispatcher; + protected $client; + protected $logger; + + abstract public function getFactory(): TransportFactoryInterface; + + abstract public function supportsProvider(): iterable; + + abstract public function createProvider(): iterable; + + public function unsupportedSchemeProvider(): iterable + { + return []; + } + + public function incompleteDsnProvider(): iterable + { + return []; + } + + /** + * @dataProvider supportsProvider + */ + public function testSupports(Dsn $dsn, bool $supports) + { + $factory = $this->getFactory(); + + $this->assertSame($supports, $factory->supports($dsn)); + } + + /** + * @dataProvider createProvider + */ + public function testCreate(Dsn $dsn, TransportInterface $transport) + { + $factory = $this->getFactory(); + + $this->assertEquals($transport, $factory->create($dsn)); + if (str_contains('smtp', $dsn->getScheme())) { + $this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', (string) $transport); + } + } + + /** + * @dataProvider unsupportedSchemeProvider + */ + public function testUnsupportedSchemeException(Dsn $dsn, string $message = null) + { + $factory = $this->getFactory(); + + $this->expectException(UnsupportedSchemeException::class); + if (null !== $message) { + $this->expectExceptionMessage($message); + } + + $factory->create($dsn); + } + + /** + * @dataProvider incompleteDsnProvider + */ + public function testIncompleteDsnException(Dsn $dsn) + { + $factory = $this->getFactory(); + + $this->expectException(IncompleteDsnException::class); + $factory->create($dsn); + } + + protected function getDispatcher(): EventDispatcherInterface + { + return $this->dispatcher ?? $this->dispatcher = $this->createMock(EventDispatcherInterface::class); + } + + protected function getClient(): HttpClientInterface + { + return $this->client ?? $this->client = $this->createMock(HttpClientInterface::class); + } + + protected function getLogger(): LoggerInterface + { + return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport.php b/plugins/email/vendor/symfony/mailer/Transport.php new file mode 100644 index 0000000..c2b813f --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\FailoverTransport; +use Symfony\Component\Mailer\Transport\NativeTransportFactory; +use Symfony\Component\Mailer\Transport\NullTransportFactory; +use Symfony\Component\Mailer\Transport\RoundRobinTransport; +use Symfony\Component\Mailer\Transport\SendmailTransportFactory; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mailer\Transport\Transports; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Fabien Potencier + * @author Konstantin Myakshin + * + * @final since Symfony 5.4 + */ +class Transport +{ + private const FACTORY_CLASSES = [ + GmailTransportFactory::class, + MailgunTransportFactory::class, + MailjetTransportFactory::class, + MandrillTransportFactory::class, + OhMySmtpTransportFactory::class, + PostmarkTransportFactory::class, + SendgridTransportFactory::class, + SendinblueTransportFactory::class, + SesTransportFactory::class, + ]; + + private $factories; + + /** + * @param EventDispatcherInterface|null $dispatcher + * @param HttpClientInterface|null $client + * @param LoggerInterface|null $logger + */ + public static function fromDsn(string $dsn/* , EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null */): TransportInterface + { + $dispatcher = 2 <= \func_num_args() ? func_get_arg(1) : null; + $client = 3 <= \func_num_args() ? func_get_arg(2) : null; + $logger = 4 <= \func_num_args() ? func_get_arg(3) : null; + + $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); + + return $factory->fromString($dsn); + } + + /** + * @param EventDispatcherInterface|null $dispatcher + * @param HttpClientInterface|null $client + * @param LoggerInterface|null $logger + */ + public static function fromDsns(array $dsns/* , EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null */): TransportInterface + { + $dispatcher = 2 <= \func_num_args() ? func_get_arg(1) : null; + $client = 3 <= \func_num_args() ? func_get_arg(2) : null; + $logger = 4 <= \func_num_args() ? func_get_arg(3) : null; + + $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); + + return $factory->fromStrings($dsns); + } + + /** + * @param TransportFactoryInterface[] $factories + */ + public function __construct(iterable $factories) + { + $this->factories = $factories; + } + + public function fromStrings(array $dsns): Transports + { + $transports = []; + foreach ($dsns as $name => $dsn) { + $transports[$name] = $this->fromString($dsn); + } + + return new Transports($transports); + } + + public function fromString(string $dsn): TransportInterface + { + [$transport, $offset] = $this->parseDsn($dsn); + if ($offset !== \strlen($dsn)) { + throw new InvalidArgumentException(sprintf('The DSN has some garbage at the end: "%s".', substr($dsn, $offset))); + } + + return $transport; + } + + private function parseDsn(string $dsn, int $offset = 0): array + { + static $keywords = [ + 'failover' => FailoverTransport::class, + 'roundrobin' => RoundRobinTransport::class, + ]; + + while (true) { + foreach ($keywords as $name => $class) { + $name .= '('; + if ($name === substr($dsn, $offset, \strlen($name))) { + $offset += \strlen($name) - 1; + preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset); + if (!isset($matches[0])) { + continue; + } + + ++$offset; + $args = []; + while (true) { + [$arg, $offset] = $this->parseDsn($dsn, $offset); + $args[] = $arg; + if (\strlen($dsn) === $offset) { + break; + } + ++$offset; + if (')' === $dsn[$offset - 1]) { + break; + } + } + + return [new $class($args), $offset]; + } + } + + if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) { + throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords)))); + } + + if ($pos = strcspn($dsn, ' )', $offset)) { + return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos]; + } + + return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)]; + } + } + + public function fromDsnObject(Dsn $dsn): TransportInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return $factory->create($dsn); + } + } + + throw new UnsupportedSchemeException($dsn); + } + + /** + * @param EventDispatcherInterface|null $dispatcher + * @param HttpClientInterface|null $client + * @param LoggerInterface|null $logger + * + * @return \Traversable + */ + public static function getDefaultFactories(/* EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null */): iterable + { + $dispatcher = 1 <= \func_num_args() ? func_get_arg(0) : null; + $client = 2 <= \func_num_args() ? func_get_arg(1) : null; + $logger = 3 <= \func_num_args() ? func_get_arg(2) : null; + + foreach (self::FACTORY_CLASSES as $factoryClass) { + if (class_exists($factoryClass)) { + yield new $factoryClass($dispatcher, $client, $logger); + } + } + + yield new NullTransportFactory($dispatcher, $client, $logger); + + yield new SendmailTransportFactory($dispatcher, $client, $logger); + + yield new EsmtpTransportFactory($dispatcher, $client, $logger); + + yield new NativeTransportFactory($dispatcher, $client, $logger); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/AbstractApiTransport.php b/plugins/email/vendor/symfony/mailer/Transport/AbstractApiTransport.php new file mode 100644 index 0000000..392484d --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/AbstractApiTransport.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\RuntimeException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\MessageConverter; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +abstract class AbstractApiTransport extends AbstractHttpTransport +{ + abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface; + + protected function doSendHttp(SentMessage $message): ResponseInterface + { + try { + $email = MessageConverter::toEmail($message->getOriginalMessage()); + } catch (\Exception $e) { + throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e); + } + + return $this->doSendApi($message, $email, $message->getEnvelope()); + } + + protected function getRecipients(Email $email, Envelope $envelope): array + { + return array_filter($envelope->getRecipients(), function (Address $address) use ($email) { + return false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true); + }); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/AbstractHttpTransport.php b/plugins/email/vendor/symfony/mailer/Transport/AbstractHttpTransport.php new file mode 100644 index 0000000..2317a0d --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/AbstractHttpTransport.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Victor Bocharsky + */ +abstract class AbstractHttpTransport extends AbstractTransport +{ + protected $host; + protected $port; + protected $client; + + public function __construct(HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + $this->client = $client; + if (null === $client) { + if (!class_exists(HttpClient::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); + } + + $this->client = HttpClient::create(); + } + + parent::__construct($dispatcher, $logger); + } + + /** + * @return $this + */ + public function setHost(?string $host) + { + $this->host = $host; + + return $this; + } + + /** + * @return $this + */ + public function setPort(?int $port) + { + $this->port = $port; + + return $this; + } + + abstract protected function doSendHttp(SentMessage $message): ResponseInterface; + + protected function doSend(SentMessage $message): void + { + $response = null; + try { + $response = $this->doSendHttp($message); + $message->appendDebug($response->getInfo('debug') ?? ''); + } catch (HttpTransportException $e) { + $e->appendDebug($e->getResponse()->getInfo('debug') ?? ''); + + throw $e; + } + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/AbstractTransport.php b/plugins/email/vendor/symfony/mailer/Transport/AbstractTransport.php new file mode 100644 index 0000000..77d810b --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/AbstractTransport.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface; + +/** + * @author Fabien Potencier + */ +abstract class AbstractTransport implements TransportInterface +{ + private $dispatcher; + private $logger; + private $rate = 0; + private $lastSent = 0; + + public function __construct(EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + $this->dispatcher = class_exists(Event::class) && $dispatcher instanceof SymfonyEventDispatcherInterface ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher; + $this->logger = $logger ?? new NullLogger(); + } + + /** + * Sets the maximum number of messages to send per second (0 to disable). + * + * @return $this + */ + public function setMaxPerSecond(float $rate): self + { + if (0 >= $rate) { + $rate = 0; + } + + $this->rate = $rate; + $this->lastSent = 0; + + return $this; + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + $message = clone $message; + $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); + + if (null !== $this->dispatcher) { + $event = new MessageEvent($message, $envelope, (string) $this); + $this->dispatcher->dispatch($event); + $envelope = $event->getEnvelope(); + } + + $message = new SentMessage($message, $envelope); + $this->doSend($message); + + $this->checkThrottling(); + + return $message; + } + + abstract protected function doSend(SentMessage $message): void; + + /** + * @param Address[] $addresses + * + * @return string[] + */ + protected function stringifyAddresses(array $addresses): array + { + return array_map(function (Address $a) { + return $a->toString(); + }, $addresses); + } + + protected function getLogger(): LoggerInterface + { + return $this->logger; + } + + private function checkThrottling() + { + if (0 == $this->rate) { + return; + } + + $sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent); + if (0 < $sleep) { + $this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep)); + usleep($sleep * 1000000); + } + $this->lastSent = microtime(true); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/AbstractTransportFactory.php b/plugins/email/vendor/symfony/mailer/Transport/AbstractTransportFactory.php new file mode 100644 index 0000000..e1617d2 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/AbstractTransportFactory.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Konstantin Myakshin + */ +abstract class AbstractTransportFactory implements TransportFactoryInterface +{ + protected $dispatcher; + protected $client; + protected $logger; + + public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null) + { + $this->dispatcher = $dispatcher; + $this->client = $client; + $this->logger = $logger; + } + + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); + } + + abstract protected function getSupportedSchemes(): array; + + protected function getUser(Dsn $dsn): string + { + $user = $dsn->getUser(); + if (null === $user) { + throw new IncompleteDsnException('User is not set.'); + } + + return $user; + } + + protected function getPassword(Dsn $dsn): string + { + $password = $dsn->getPassword(); + if (null === $password) { + throw new IncompleteDsnException('Password is not set.'); + } + + return $password; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Dsn.php b/plugins/email/vendor/symfony/mailer/Transport/Dsn.php new file mode 100644 index 0000000..04d3540 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Dsn.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\InvalidArgumentException; + +/** + * @author Konstantin Myakshin + */ +final class Dsn +{ + private $scheme; + private $host; + private $user; + private $password; + private $port; + private $options; + + public function __construct(string $scheme, string $host, string $user = null, string $password = null, int $port = null, array $options = []) + { + $this->scheme = $scheme; + $this->host = $host; + $this->user = $user; + $this->password = $password; + $this->port = $port; + $this->options = $options; + } + + public static function fromString(string $dsn): self + { + if (false === $parsedDsn = parse_url($dsn)) { + throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn)); + } + + if (!isset($parsedDsn['scheme'])) { + throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a scheme.', $dsn)); + } + + if (!isset($parsedDsn['host'])) { + throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a host (use "default" by default).', $dsn)); + } + + $user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; + $password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; + $port = $parsedDsn['port'] ?? null; + parse_str($parsedDsn['query'] ?? '', $query); + + return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/FailoverTransport.php b/plugins/email/vendor/symfony/mailer/Transport/FailoverTransport.php new file mode 100644 index 0000000..4913901 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/FailoverTransport.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +/** + * Uses several Transports using a failover algorithm. + * + * @author Fabien Potencier + */ +class FailoverTransport extends RoundRobinTransport +{ + private $currentTransport; + + protected function getNextTransport(): ?TransportInterface + { + if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) { + $this->currentTransport = parent::getNextTransport(); + } + + return $this->currentTransport; + } + + protected function getInitialCursor(): int + { + return 0; + } + + protected function getNameSymbol(): string + { + return 'failover'; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/NativeTransportFactory.php b/plugins/email/vendor/symfony/mailer/Transport/NativeTransportFactory.php new file mode 100644 index 0000000..8afa53c --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/NativeTransportFactory.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; + +/** + * Factory that configures a transport (sendmail or SMTP) based on php.ini settings. + * + * @author Laurent VOULLEMIER + */ +final class NativeTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { + throw new UnsupportedSchemeException($dsn, 'native', $this->getSupportedSchemes()); + } + + if ($sendMailPath = ini_get('sendmail_path')) { + return new SendmailTransport($sendMailPath, $this->dispatcher, $this->logger); + } + + if ('\\' !== \DIRECTORY_SEPARATOR) { + throw new TransportException('sendmail_path is not configured in php.ini.'); + } + + // Only for windows hosts; at this point non-windows + // host have already thrown an exception or returned a transport + $host = ini_get('SMTP'); + $port = (int) ini_get('smtp_port'); + + if (!$host || !$port) { + throw new TransportException('smtp or smtp_port is not configured in php.ini.'); + } + + $socketStream = new SocketStream(); + $socketStream->setHost($host); + $socketStream->setPort($port); + if (465 !== $port) { + $socketStream->disableTls(); + } + + return new SmtpTransport($socketStream, $this->dispatcher, $this->logger); + } + + protected function getSupportedSchemes(): array + { + return ['native']; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/NullTransport.php b/plugins/email/vendor/symfony/mailer/Transport/NullTransport.php new file mode 100644 index 0000000..92fb82a --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/NullTransport.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\SentMessage; + +/** + * Pretends messages have been sent, but just ignores them. + * + * @author Fabien Potencier + */ +final class NullTransport extends AbstractTransport +{ + protected function doSend(SentMessage $message): void + { + } + + public function __toString(): string + { + return 'null://'; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/NullTransportFactory.php b/plugins/email/vendor/symfony/mailer/Transport/NullTransportFactory.php new file mode 100644 index 0000000..4c45f39 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/NullTransportFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +final class NullTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('null' === $dsn->getScheme()) { + return new NullTransport($this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['null']; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/RoundRobinTransport.php b/plugins/email/vendor/symfony/mailer/Transport/RoundRobinTransport.php new file mode 100644 index 0000000..2f35138 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/RoundRobinTransport.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\RawMessage; + +/** + * Uses several Transports using a round robin algorithm. + * + * @author Fabien Potencier + */ +class RoundRobinTransport implements TransportInterface +{ + /** + * @var \SplObjectStorage + */ + private $deadTransports; + private $transports = []; + private $retryPeriod; + private $cursor = -1; + + /** + * @param TransportInterface[] $transports + */ + public function __construct(array $transports, int $retryPeriod = 60) + { + if (!$transports) { + throw new TransportException(sprintf('"%s" must have at least one transport configured.', static::class)); + } + + $this->transports = $transports; + $this->deadTransports = new \SplObjectStorage(); + $this->retryPeriod = $retryPeriod; + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + while ($transport = $this->getNextTransport()) { + try { + return $transport->send($message, $envelope); + } catch (TransportExceptionInterface $e) { + $this->deadTransports[$transport] = microtime(true); + } + } + + throw new TransportException('All transports failed.'); + } + + public function __toString(): string + { + return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')'; + } + + /** + * Rotates the transport list around and returns the first instance. + */ + protected function getNextTransport(): ?TransportInterface + { + if (-1 === $this->cursor) { + $this->cursor = $this->getInitialCursor(); + } + + $cursor = $this->cursor; + while (true) { + $transport = $this->transports[$cursor]; + + if (!$this->isTransportDead($transport)) { + break; + } + + if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) { + $this->deadTransports->detach($transport); + + break; + } + + if ($this->cursor === $cursor = $this->moveCursor($cursor)) { + return null; + } + } + + $this->cursor = $this->moveCursor($cursor); + + return $transport; + } + + protected function isTransportDead(TransportInterface $transport): bool + { + return $this->deadTransports->contains($transport); + } + + protected function getInitialCursor(): int + { + // the cursor initial value is randomized so that + // when are not in a daemon, we are still rotating the transports + return mt_rand(0, \count($this->transports) - 1); + } + + protected function getNameSymbol(): string + { + return 'roundrobin'; + } + + private function moveCursor(int $cursor): int + { + return ++$cursor >= \count($this->transports) ? 0 : $cursor; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/SendmailTransport.php b/plugins/email/vendor/symfony/mailer/Transport/SendmailTransport.php new file mode 100644 index 0000000..43d0920 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/SendmailTransport.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; +use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream; +use Symfony\Component\Mime\RawMessage; + +/** + * SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary. + * + * Transport can be instanciated through SendmailTransportFactory or NativeTransportFactory: + * + * - SendmailTransportFactory to use most common sendmail path and recommanded options + * - NativeTransportFactory when configuration is set via php.ini + * + * @author Fabien Potencier + * @author Chris Corbyn + */ +class SendmailTransport extends AbstractTransport +{ + private $command = '/usr/sbin/sendmail -bs'; + private $stream; + private $transport; + + /** + * Constructor. + * + * Supported modes are -bs and -t, with any additional flags desired. + * + * The recommended mode is "-bs" since it is interactive and failure notifications are hence possible. + * Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed). + * + * If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t) + * + * -f flag will be appended automatically if one is not present. + */ + public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + parent::__construct($dispatcher, $logger); + + if (null !== $command) { + if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) { + throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command)); + } + + $this->command = $command; + } + + $this->stream = new ProcessStream(); + if (str_contains($this->command, ' -bs')) { + $this->stream->setCommand($this->command); + $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger); + } + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + if ($this->transport) { + return $this->transport->send($message, $envelope); + } + + return parent::send($message, $envelope); + } + + public function __toString(): string + { + if ($this->transport) { + return (string) $this->transport; + } + + return 'smtp://sendmail'; + } + + protected function doSend(SentMessage $message): void + { + $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); + + $command = $this->command; + + if ($recipients = $message->getEnvelope()->getRecipients()) { + $command = str_replace(' -t', '', $command); + } + + if (!str_contains($command, ' -f')) { + $command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress()); + } + + $chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable()); + + if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) { + $chunks = AbstractStream::replace("\n.", "\n..", $chunks); + } + + foreach ($recipients as $recipient) { + $command .= ' '.escapeshellarg($recipient->getEncodedAddress()); + } + + $this->stream->setCommand($command); + $this->stream->initialize(); + foreach ($chunks as $chunk) { + $this->stream->write($chunk); + } + $this->stream->flush(); + $this->stream->terminate(); + + $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/SendmailTransportFactory.php b/plugins/email/vendor/symfony/mailer/Transport/SendmailTransportFactory.php new file mode 100644 index 0000000..6d977e7 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/SendmailTransportFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +final class SendmailTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) { + return new SendmailTransport($dsn->getOption('command'), $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'sendmail', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['sendmail', 'sendmail+smtp']; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php new file mode 100644 index 0000000..98ea2d4 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * An Authentication mechanism. + * + * @author Chris Corbyn + */ +interface AuthenticatorInterface +{ + /** + * Tries to authenticate the user. + * + * @throws TransportExceptionInterface + */ + public function authenticate(EsmtpTransport $client): void; + + /** + * Gets the name of the AUTH mechanism this Authenticator handles. + */ + public function getAuthKeyword(): string; +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php new file mode 100644 index 0000000..b2ec7b0 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles CRAM-MD5 authentication. + * + * @author Chris Corbyn + */ +class CramMd5Authenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'CRAM-MD5'; + } + + /** + * {@inheritdoc} + * + * @see https://www.ietf.org/rfc/rfc4954.txt + */ + public function authenticate(EsmtpTransport $client): void + { + $challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]); + $challenge = base64_decode(substr($challenge, 4)); + $message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge)); + $client->executeCommand(sprintf("%s\r\n", $message), [235]); + } + + /** + * Generates a CRAM-MD5 response from a server challenge. + */ + private function getResponse(string $secret, string $challenge): string + { + if (\strlen($secret) > 64) { + $secret = pack('H32', md5($secret)); + } + + if (\strlen($secret) < 64) { + $secret = str_pad($secret, 64, \chr(0)); + } + + $kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64); + $kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64); + + $inner = pack('H32', md5($kipad.$challenge)); + $digest = md5($kopad.$inner); + + return $digest; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php new file mode 100644 index 0000000..1ce321d --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles LOGIN authentication. + * + * @author Chris Corbyn + */ +class LoginAuthenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'LOGIN'; + } + + /** + * {@inheritdoc} + * + * @see https://www.ietf.org/rfc/rfc4954.txt + */ + public function authenticate(EsmtpTransport $client): void + { + $client->executeCommand("AUTH LOGIN\r\n", [334]); + $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]); + $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php new file mode 100644 index 0000000..8d60690 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles PLAIN authentication. + * + * @author Chris Corbyn + */ +class PlainAuthenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'PLAIN'; + } + + /** + * {@inheritdoc} + * + * @see https://www.ietf.org/rfc/rfc4954.txt + */ + public function authenticate(EsmtpTransport $client): void + { + $client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php new file mode 100644 index 0000000..7941776 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles XOAUTH2 authentication. + * + * @author xu.li + * + * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol + */ +class XOAuth2Authenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'XOAUTH2'; + } + + /** + * {@inheritdoc} + * + * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism + */ + public function authenticate(EsmtpTransport $client): void + { + $client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/EsmtpTransport.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/EsmtpTransport.php new file mode 100644 index 0000000..1dcb53f --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/EsmtpTransport.php @@ -0,0 +1,200 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; + +/** + * Sends Emails over SMTP with ESMTP support. + * + * @author Fabien Potencier + * @author Chris Corbyn + */ +class EsmtpTransport extends SmtpTransport +{ + private $authenticators = []; + private $username = ''; + private $password = ''; + + public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + parent::__construct(null, $dispatcher, $logger); + + // order is important here (roughly most secure and popular first) + $this->authenticators = [ + new Auth\CramMd5Authenticator(), + new Auth\LoginAuthenticator(), + new Auth\PlainAuthenticator(), + new Auth\XOAuth2Authenticator(), + ]; + + /** @var SocketStream $stream */ + $stream = $this->getStream(); + + if (null === $tls) { + if (465 === $port) { + $tls = true; + } else { + $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host; + } + } + if (!$tls) { + $stream->disableTls(); + } + if (0 === $port) { + $port = $tls ? 465 : 25; + } + + $stream->setHost($host); + $stream->setPort($port); + } + + /** + * @return $this + */ + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + public function getUsername(): string + { + return $this->username; + } + + /** + * @return $this + */ + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + public function getPassword(): string + { + return $this->password; + } + + public function addAuthenticator(AuthenticatorInterface $authenticator): void + { + $this->authenticators[] = $authenticator; + } + + protected function doHeloCommand(): void + { + if (!$capabilities = $this->callHeloCommand()) { + return; + } + + /** @var SocketStream $stream */ + $stream = $this->getStream(); + // WARNING: !$stream->isTLS() is right, 100% sure :) + // if you think that the ! should be removed, read the code again + // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured + if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) { + $this->executeCommand("STARTTLS\r\n", [220]); + + if (!$stream->startTLS()) { + throw new TransportException('Unable to connect with STARTTLS.'); + } + + $capabilities = $this->callHeloCommand(); + } + + if (\array_key_exists('AUTH', $capabilities)) { + $this->handleAuth($capabilities['AUTH']); + } + } + + private function callHeloCommand(): array + { + try { + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $e) { + try { + parent::doHeloCommand(); + + return []; + } catch (TransportExceptionInterface $ex) { + if (!$ex->getCode()) { + throw $e; + } + + throw $ex; + } + } + + $capabilities = []; + $lines = explode("\r\n", trim($response)); + array_shift($lines); + foreach ($lines as $line) { + if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { + $value = strtoupper(ltrim($matches[2], ' =')); + $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : []; + } + } + + return $capabilities; + } + + private function handleAuth(array $modes): void + { + if (!$this->username) { + return; + } + + $authNames = []; + $errors = []; + $modes = array_map('strtolower', $modes); + foreach ($this->authenticators as $authenticator) { + if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { + continue; + } + + $authNames[] = $authenticator->getAuthKeyword(); + try { + $authenticator->authenticate($this); + + return; + } catch (TransportExceptionInterface $e) { + try { + $this->executeCommand("RSET\r\n", [250]); + } catch (TransportExceptionInterface $_) { + // ignore this exception as it probably means that the server error was final + } + + // keep the error message, but tries the other authenticators + $errors[$authenticator->getAuthKeyword()] = $e->getMessage(); + } + } + + if (!$authNames) { + throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes))); + } + + $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames)); + foreach ($errors as $name => $error) { + $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error); + } + + throw new TransportException($message); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php new file mode 100644 index 0000000..e2a280e --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class EsmtpTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $tls = 'smtps' === $dsn->getScheme() ? true : null; + $port = $dsn->getPort(0); + $host = $dsn->getHost(); + + $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); + + if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOLEAN)) { + /** @var SocketStream $stream */ + $stream = $transport->getStream(); + $streamOptions = $stream->getStreamOptions(); + + $streamOptions['ssl']['verify_peer'] = false; + $streamOptions['ssl']['verify_peer_name'] = false; + + $stream->setStreamOptions($streamOptions); + } + + if ($user = $dsn->getUser()) { + $transport->setUsername($user); + } + + if ($password = $dsn->getPassword()) { + $transport->setPassword($password); + } + + if (null !== ($localDomain = $dsn->getOption('local_domain'))) { + $transport->setLocalDomain($localDomain); + } + + if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) { + $transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0)); + } + + if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) { + $transport->setPingThreshold((int) $pingThreshold); + } + + return $transport; + } + + protected function getSupportedSchemes(): array + { + return ['smtp', 'smtps']; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php new file mode 100644 index 0000000..92af6aa --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php @@ -0,0 +1,361 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractTransport; +use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; +use Symfony\Component\Mime\RawMessage; + +/** + * Sends emails over SMTP. + * + * @author Fabien Potencier + * @author Chris Corbyn + */ +class SmtpTransport extends AbstractTransport +{ + private $started = false; + private $restartThreshold = 100; + private $restartThresholdSleep = 0; + private $restartCounter; + private $pingThreshold = 100; + private $lastMessageTime = 0; + private $stream; + private $domain = '[127.0.0.1]'; + + public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + parent::__construct($dispatcher, $logger); + + $this->stream = $stream ?? new SocketStream(); + } + + public function getStream(): AbstractStream + { + return $this->stream; + } + + /** + * Sets the maximum number of messages to send before re-starting the transport. + * + * By default, the threshold is set to 100 (and no sleep at restart). + * + * @param int $threshold The maximum number of messages (0 to disable) + * @param int $sleep The number of seconds to sleep between stopping and re-starting the transport + * + * @return $this + */ + public function setRestartThreshold(int $threshold, int $sleep = 0): self + { + $this->restartThreshold = $threshold; + $this->restartThresholdSleep = $sleep; + + return $this; + } + + /** + * Sets the minimum number of seconds required between two messages, before the server is pinged. + * If the transport wants to send a message and the time since the last message exceeds the specified threshold, + * the transport will ping the server first (NOOP command) to check if the connection is still alive. + * Otherwise the message will be sent without pinging the server first. + * + * Do not set the threshold too low, as the SMTP server may drop the connection if there are too many + * non-mail commands (like pinging the server with NOOP). + * + * By default, the threshold is set to 100 seconds. + * + * @param int $seconds The minimum number of seconds between two messages required to ping the server + * + * @return $this + */ + public function setPingThreshold(int $seconds): self + { + $this->pingThreshold = $seconds; + + return $this; + } + + /** + * Sets the name of the local domain that will be used in HELO. + * + * This should be a fully-qualified domain name and should be truly the domain + * you're using. + * + * If your server does not have a domain name, use the IP address. This will + * automatically be wrapped in square brackets as described in RFC 5321, + * section 4.1.3. + * + * @return $this + */ + public function setLocalDomain(string $domain): self + { + if ('' !== $domain && '[' !== $domain[0]) { + if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + $domain = '['.$domain.']'; + } elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + $domain = '[IPv6:'.$domain.']'; + } + } + + $this->domain = $domain; + + return $this; + } + + /** + * Gets the name of the domain that will be used in HELO. + * + * If an IP address was specified, this will be returned wrapped in square + * brackets as described in RFC 5321, section 4.1.3. + */ + public function getLocalDomain(): string + { + return $this->domain; + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + try { + $message = parent::send($message, $envelope); + } catch (TransportExceptionInterface $e) { + if ($this->started) { + try { + $this->executeCommand("RSET\r\n", [250]); + } catch (TransportExceptionInterface $_) { + // ignore this exception as it probably means that the server error was final + } + } + + throw $e; + } + + $this->checkRestartThreshold(); + + return $message; + } + + public function __toString(): string + { + if ($this->stream instanceof SocketStream) { + $name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost()); + $port = $this->stream->getPort(); + if (!(25 === $port || ($tls && 465 === $port))) { + $name .= ':'.$port; + } + + return $name; + } + + return 'smtp://sendmail'; + } + + /** + * Runs a command against the stream, expecting the given response codes. + * + * @param int[] $codes + * + * @throws TransportException when an invalid response if received + * + * @internal + */ + public function executeCommand(string $command, array $codes): string + { + $this->stream->write($command); + $response = $this->getFullResponse(); + $this->assertResponseCode($response, $codes); + + return $response; + } + + protected function doSend(SentMessage $message): void + { + if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) { + $this->ping(); + } + + if (!$this->started) { + $this->start(); + } + + try { + $envelope = $message->getEnvelope(); + $this->doMailFromCommand($envelope->getSender()->getEncodedAddress()); + foreach ($envelope->getRecipients() as $recipient) { + $this->doRcptToCommand($recipient->getEncodedAddress()); + } + + $this->executeCommand("DATA\r\n", [354]); + try { + foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) { + $this->stream->write($chunk, false); + } + $this->stream->flush(); + } catch (TransportExceptionInterface $e) { + throw $e; + } catch (\Exception $e) { + $this->stream->terminate(); + $this->started = false; + $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); + throw $e; + } + $this->executeCommand("\r\n.\r\n", [250]); + $message->appendDebug($this->stream->getDebug()); + $this->lastMessageTime = microtime(true); + } catch (TransportExceptionInterface $e) { + $e->appendDebug($this->stream->getDebug()); + $this->lastMessageTime = 0; + throw $e; + } + } + + protected function doHeloCommand(): void + { + $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]); + } + + private function doMailFromCommand(string $address): void + { + $this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]); + } + + private function doRcptToCommand(string $address): void + { + $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]); + } + + private function start(): void + { + if ($this->started) { + return; + } + + $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); + + $this->stream->initialize(); + $this->assertResponseCode($this->getFullResponse(), [220]); + $this->doHeloCommand(); + $this->started = true; + $this->lastMessageTime = 0; + + $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__)); + } + + private function stop(): void + { + if (!$this->started) { + return; + } + + $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__)); + + try { + $this->executeCommand("QUIT\r\n", [221]); + } catch (TransportExceptionInterface $e) { + } finally { + $this->stream->terminate(); + $this->started = false; + $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); + } + } + + private function ping(): void + { + if (!$this->started) { + return; + } + + try { + $this->executeCommand("NOOP\r\n", [250]); + } catch (TransportExceptionInterface $e) { + $this->stop(); + } + } + + /** + * @throws TransportException if a response code is incorrect + */ + private function assertResponseCode(string $response, array $codes): void + { + if (!$codes) { + throw new LogicException('You must set the expected response code.'); + } + + [$code] = sscanf($response, '%3d'); + $valid = \in_array($code, $codes); + + if (!$valid || !$response) { + $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code'; + $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : ''; + + throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0); + } + } + + private function getFullResponse(): string + { + $response = ''; + do { + $line = $this->stream->readLine(); + $response .= $line; + } while ($line && isset($line[3]) && ' ' !== $line[3]); + + return $response; + } + + private function checkRestartThreshold(): void + { + // when using sendmail via non-interactive mode, the transport is never "started" + if (!$this->started) { + return; + } + + ++$this->restartCounter; + if ($this->restartCounter < $this->restartThreshold) { + return; + } + + $this->stop(); + if (0 < $sleep = $this->restartThresholdSleep) { + $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep)); + + sleep($sleep); + } + $this->start(); + $this->restartCounter = 0; + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->stop(); + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php new file mode 100644 index 0000000..28cb616 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Stream; + +use Symfony\Component\Mailer\Exception\TransportException; + +/** + * A stream supporting remote sockets and local processes. + * + * @author Fabien Potencier + * @author Nicolas Grekas + * @author Chris Corbyn + * + * @internal + */ +abstract class AbstractStream +{ + protected $stream; + protected $in; + protected $out; + + private $debug = ''; + + public function write(string $bytes, bool $debug = true): void + { + if ($debug) { + foreach (explode("\n", trim($bytes)) as $line) { + $this->debug .= sprintf("> %s\n", $line); + } + } + + $bytesToWrite = \strlen($bytes); + $totalBytesWritten = 0; + while ($totalBytesWritten < $bytesToWrite) { + $bytesWritten = @fwrite($this->in, substr($bytes, $totalBytesWritten)); + if (false === $bytesWritten || 0 === $bytesWritten) { + throw new TransportException('Unable to write bytes on the wire.'); + } + + $totalBytesWritten += $bytesWritten; + } + } + + /** + * Flushes the contents of the stream (empty it) and set the internal pointer to the beginning. + */ + public function flush(): void + { + fflush($this->in); + } + + /** + * Performs any initialization needed. + */ + abstract public function initialize(): void; + + public function terminate(): void + { + $this->stream = $this->out = $this->in = null; + } + + public function readLine(): string + { + if (feof($this->out)) { + return ''; + } + + $line = fgets($this->out); + if ('' === $line) { + $metas = stream_get_meta_data($this->out); + if ($metas['timed_out']) { + throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription())); + } + if ($metas['eof']) { + throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription())); + } + } + + $this->debug .= sprintf('< %s', $line); + + return $line; + } + + public function getDebug(): string + { + $debug = $this->debug; + $this->debug = ''; + + return $debug; + } + + public static function replace(string $from, string $to, iterable $chunks): \Generator + { + if ('' === $from) { + yield from $chunks; + + return; + } + + $carry = ''; + $fromLen = \strlen($from); + + foreach ($chunks as $chunk) { + if ('' === $chunk = $carry.$chunk) { + continue; + } + + if (str_contains($chunk, $from)) { + $chunk = explode($from, $chunk); + $carry = array_pop($chunk); + + yield implode($to, $chunk).$to; + } else { + $carry = $chunk; + } + + if (\strlen($carry) > $fromLen) { + yield substr($carry, 0, -$fromLen); + $carry = substr($carry, -$fromLen); + } + } + + if ('' !== $carry) { + yield $carry; + } + } + + abstract protected function getReadConnectionDescription(): string; +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php new file mode 100644 index 0000000..a8a8603 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Stream; + +use Symfony\Component\Mailer\Exception\TransportException; + +/** + * A stream supporting local processes. + * + * @author Fabien Potencier + * @author Chris Corbyn + * + * @internal + */ +final class ProcessStream extends AbstractStream +{ + private $command; + + public function setCommand(string $command) + { + $this->command = $command; + } + + public function initialize(): void + { + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $pipes = []; + $this->stream = proc_open($this->command, $descriptorSpec, $pipes); + stream_set_blocking($pipes[2], false); + if ($err = stream_get_contents($pipes[2])) { + throw new TransportException('Process could not be started: '.$err); + } + $this->in = &$pipes[0]; + $this->out = &$pipes[1]; + } + + public function terminate(): void + { + if (null !== $this->stream) { + fclose($this->in); + fclose($this->out); + proc_close($this->stream); + } + + parent::terminate(); + } + + protected function getReadConnectionDescription(): string + { + return 'process '.$this->command; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/SocketStream.php b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/SocketStream.php new file mode 100644 index 0000000..144ab7f --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Smtp/Stream/SocketStream.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Stream; + +use Symfony\Component\Mailer\Exception\TransportException; + +/** + * A stream supporting remote sockets. + * + * @author Fabien Potencier + * @author Chris Corbyn + * + * @internal + */ +final class SocketStream extends AbstractStream +{ + private $url; + private $host = 'localhost'; + private $port = 465; + private $timeout; + private $tls = true; + private $sourceIp; + private $streamContextOptions = []; + + /** + * @return $this + */ + public function setTimeout(float $timeout): self + { + $this->timeout = $timeout; + + return $this; + } + + public function getTimeout(): float + { + return $this->timeout ?? (float) \ini_get('default_socket_timeout'); + } + + /** + * Literal IPv6 addresses should be wrapped in square brackets. + * + * @return $this + */ + public function setHost(string $host): self + { + $this->host = $host; + + return $this; + } + + public function getHost(): string + { + return $this->host; + } + + /** + * @return $this + */ + public function setPort(int $port): self + { + $this->port = $port; + + return $this; + } + + public function getPort(): int + { + return $this->port; + } + + /** + * Sets the TLS/SSL on the socket (disables STARTTLS). + * + * @return $this + */ + public function disableTls(): self + { + $this->tls = false; + + return $this; + } + + public function isTLS(): bool + { + return $this->tls; + } + + /** + * @return $this + */ + public function setStreamOptions(array $options): self + { + $this->streamContextOptions = $options; + + return $this; + } + + public function getStreamOptions(): array + { + return $this->streamContextOptions; + } + + /** + * Sets the source IP. + * + * IPv6 addresses should be wrapped in square brackets. + * + * @return $this + */ + public function setSourceIp(string $ip): self + { + $this->sourceIp = $ip; + + return $this; + } + + /** + * Returns the IP used to connect to the destination. + */ + public function getSourceIp(): ?string + { + return $this->sourceIp; + } + + public function initialize(): void + { + $this->url = $this->host.':'.$this->port; + if ($this->tls) { + $this->url = 'ssl://'.$this->url; + } + $options = []; + if ($this->sourceIp) { + $options['socket']['bindto'] = $this->sourceIp.':0'; + } + if ($this->streamContextOptions) { + $options = array_merge($options, $this->streamContextOptions); + } + // do it unconditionnally as it will be used by STARTTLS as well if supported + $options['ssl']['crypto_method'] = $options['ssl']['crypto_method'] ?? \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + $streamContext = stream_context_create($options); + + $timeout = $this->getTimeout(); + set_error_handler(function ($type, $msg) { + throw new TransportException(sprintf('Connection could not be established with host "%s": ', $this->url).$msg); + }); + try { + $this->stream = stream_socket_client($this->url, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $streamContext); + } finally { + restore_error_handler(); + } + + stream_set_blocking($this->stream, true); + stream_set_timeout($this->stream, $timeout); + $this->in = &$this->stream; + $this->out = &$this->stream; + } + + public function startTLS(): bool + { + set_error_handler(function ($type, $msg) { + throw new TransportException('Unable to connect with STARTTLS: '.$msg); + }); + try { + return stream_socket_enable_crypto($this->stream, true); + } finally { + restore_error_handler(); + } + } + + public function terminate(): void + { + if (null !== $this->stream) { + fclose($this->stream); + } + + parent::terminate(); + } + + protected function getReadConnectionDescription(): string + { + return $this->url; + } +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/TransportFactoryInterface.php b/plugins/email/vendor/symfony/mailer/Transport/TransportFactoryInterface.php new file mode 100644 index 0000000..9785ae8 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/TransportFactoryInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +interface TransportFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): TransportInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/TransportInterface.php b/plugins/email/vendor/symfony/mailer/Transport/TransportInterface.php new file mode 100644 index 0000000..ed562cf --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/TransportInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\RawMessage; + +/** + * Interface for all mailer transports. + * + * When sending emails, you should prefer MailerInterface implementations + * as they allow asynchronous sending. + * + * @author Fabien Potencier + */ +interface TransportInterface +{ + /** + * @throws TransportExceptionInterface + */ + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage; + + public function __toString(): string; +} diff --git a/plugins/email/vendor/symfony/mailer/Transport/Transports.php b/plugins/email/vendor/symfony/mailer/Transport/Transports.php new file mode 100644 index 0000000..702fc5c --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/Transport/Transports.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +final class Transports implements TransportInterface +{ + private $transports; + private $default; + + /** + * @param TransportInterface[] $transports + */ + public function __construct(iterable $transports) + { + $this->transports = []; + foreach ($transports as $name => $transport) { + if (null === $this->default) { + $this->default = $transport; + } + $this->transports[$name] = $transport; + } + + if (!$this->transports) { + throw new LogicException(sprintf('"%s" must have at least one transport configured.', __CLASS__)); + } + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + /** @var Message $message */ + if (RawMessage::class === \get_class($message) || !$message->getHeaders()->has('X-Transport')) { + return $this->default->send($message, $envelope); + } + + $headers = $message->getHeaders(); + $transport = $headers->get('X-Transport')->getBody(); + $headers->remove('X-Transport'); + + if (!isset($this->transports[$transport])) { + throw new InvalidArgumentException(sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports)))); + } + + try { + return $this->transports[$transport]->send($message, $envelope); + } catch (\Throwable $e) { + $headers->addTextHeader('X-Transport', $transport); + + throw $e; + } + } + + public function __toString(): string + { + return '['.implode(',', array_keys($this->transports)).']'; + } +} diff --git a/plugins/email/vendor/symfony/mailer/composer.json b/plugins/email/vendor/symfony/mailer/composer.json new file mode 100644 index 0000000..53cf0f5 --- /dev/null +++ b/plugins/email/vendor/symfony/mailer/composer.json @@ -0,0 +1,43 @@ +{ + "name": "symfony/mailer", + "type": "library", + "description": "Helps sending emails", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "egulias/email-validator": "^2.1.10|^3", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.2.6|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "require-dev": { + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/messenger": "^4.4|^5.0|^6.0" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mailer\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/messenger/Attribute/AsMessageHandler.php b/plugins/email/vendor/symfony/messenger/Attribute/AsMessageHandler.php new file mode 100644 index 0000000..8c12a80 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Attribute/AsMessageHandler.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Attribute; + +/** + * Service tag to autoconfigure message handlers. + * + * @author Alireza Mirsepassi + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsMessageHandler +{ + public function __construct( + public ?string $bus = null, + public ?string $fromTransport = null, + public ?string $handles = null, + public ?string $method = null, + public int $priority = 0, + ) { + } +} diff --git a/plugins/email/vendor/symfony/messenger/CHANGELOG.md b/plugins/email/vendor/symfony/messenger/CHANGELOG.md new file mode 100644 index 0000000..ede59fe --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/CHANGELOG.md @@ -0,0 +1,212 @@ +CHANGELOG +========= + +5.4 +--- + + * Add `AsMessageHandler` attribute for declaring message handlers on PHP 8. + * Add support for handling messages in batches with `BatchHandlerInterface` and corresponding trait + * Add `StopWorkerExceptionInterface` and its implementation `StopWorkerException` to stop the worker. + * Add support for resetting container services after each messenger message. + * Added `WorkerMetadata` class which allows you to access the configuration details of a worker, like `queueNames` and `transportNames` it consumes from. + * New method `getMetadata()` was added to `Worker` class which returns the `WorkerMetadata` object. + * Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0 + * Add log when worker should stop. + * Add log when `SIGTERM` is received. + +5.3 +--- + + * Add the `RouterContextMiddleware` to restore the original router context when handling a message + * `InMemoryTransport` can perform message serialization through dsn `in-memory://?serialize=true`. + * Added `queues` option to `Worker` to only fetch messages from a specific queue from a receiver implementing `QueueReceiverInterface`. + +5.2.0 +----- + + * The `RedeliveryStamp` will no longer be populated with error data. This information is now stored in the `ErrorDetailsStamp` instead. + * Added `FlattenExceptionNormalizer` to give more information about the exception on Messenger background processes. The `FlattenExceptionNormalizer` has a higher priority than `ProblemNormalizer` and it is only used when the Messenger serialization context is set. + * Added factory methods `DelayStamp::delayFor(\DateInterval)` and `DelayStamp::delayUntil(\DateTimeInterface)`. + * Removed the exception when dispatching a message with a `DispatchAfterCurrentBusStamp` and not in a context of another dispatch call + * Added `WorkerMessageRetriedEvent` + * Added `WorkerMessageReceivedEvent::setEnvelope()` and made event mutable + +5.1.0 +----- + + * Moved AmqpExt transport to package `symfony/amqp-messenger`. All classes in `Symfony\Component\Messenger\Transport\AmqpExt` have been moved to `Symfony\Component\Messenger\Bridge\Amqp\Transport` + * Moved Doctrine transport to package `symfony/doctrine-messenger`. All classes in `Symfony\Component\Messenger\Transport\Doctrine` have been moved to `Symfony\Component\Messenger\Bridge\Doctrine\Transport` + * Moved RedisExt transport to package `symfony/redis-messenger`. All classes in `Symfony\Component\Messenger\Transport\RedisExt` have been moved to `Symfony\Component\Messenger\Bridge\Redis\Transport` + * Added support for passing a `\Throwable` argument to `RetryStrategyInterface` methods. This allows to define strategies based on the reason of the handling failure. + * Added `StopWorkerOnFailureLimitListener` to stop the worker after a specified amount of failed messages is reached. + * Added `RecoverableExceptionInterface` interface to force retry. + +5.0.0 +----- + + * The `LoggingMiddleware` class has been removed, pass a logger to `SendMessageMiddleware` instead. + * made `SendersLocator` require a `ContainerInterface` as 2nd argument + +4.4.0 +----- + + * Added support for auto trimming of Redis streams. + * `InMemoryTransport` handle acknowledged and rejected messages. + * Made all dispatched worker event classes final. + * Added support for `from_transport` attribute on `messenger.message_handler` tag. + * Added support for passing `dbindex` as a query parameter to the redis transport DSN. + * Added `WorkerStartedEvent` and `WorkerRunningEvent` + * [BC BREAK] Removed `SendersLocatorInterface::getSenderByAlias` added in 4.3. + * [BC BREAK] Removed `$retryStrategies` argument from `Worker::__construct`. + * [BC BREAK] Changed arguments of `ConsumeMessagesCommand::__construct`. + * [BC BREAK] Removed `$senderClassOrAlias` argument from `RedeliveryStamp::__construct`. + * [BC BREAK] Removed `UnknownSenderException`. + * [BC BREAK] Removed `WorkerInterface`. + * [BC BREAK] Removed `$onHandledCallback` of `Worker::run(array $options = [], callable $onHandledCallback = null)`. + * [BC BREAK] Removed `StopWhenMemoryUsageIsExceededWorker` in favor of `StopWorkerOnMemoryLimitListener`. + * [BC BREAK] Removed `StopWhenMessageCountIsExceededWorker` in favor of `StopWorkerOnMessageLimitListener`. + * [BC BREAK] Removed `StopWhenTimeLimitIsReachedWorker` in favor of `StopWorkerOnTimeLimitListener`. + * [BC BREAK] Removed `StopWhenRestartSignalIsReceived` in favor of `StopWorkerOnRestartSignalListener`. + * The component is not marked as `@experimental` anymore. + * Marked the `MessengerDataCollector` class as `@final`. + * Added support for `DelayStamp` to the `redis` transport. + +4.3.0 +----- + + * Added `NonSendableStampInterface` that a stamp can implement if + it should not be sent to a transport. Transport serializers + must now check for these stamps and not encode them. + * [BC BREAK] `SendersLocatorInterface` has an additional method: + `getSenderByAlias()`. + * Removed argument `?bool &$handle = false` from `SendersLocatorInterface::getSenders` + * A new `ListableReceiverInterface` was added, which a receiver + can implement (when applicable) to enable listing and fetching + individual messages by id (used in the new "Failed Messages" commands). + * Both `SenderInterface::send()` and `ReceiverInterface::get()` + should now (when applicable) add a `TransportMessageIdStamp`. + * Added `WorkerStoppedEvent` dispatched when a worker is stopped. + * Added optional `MessageCountAwareInterface` that receivers can implement + to give information about how many messages are waiting to be processed. + * [BC BREAK] The `Envelope::__construct()` signature changed: + you can no longer pass an unlimited number of stamps as the second, + third, fourth, arguments etc: stamps are now an array passed to the + second argument. + * [BC BREAK] The `MessageBusInterface::dispatch()` signature changed: + a second argument `array $stamps = []` was added. + * Added new `messenger:stop-workers` command that sends a signal + to stop all `messenger:consume` workers. + * [BC BREAK] The `TransportFactoryInterface::createTransport()` signature + changed: a required 3rd `SerializerInterface` argument was added. + * Added a new `SyncTransport` to explicitly handle messages synchronously. + * Added `AmqpStamp` allowing to provide a routing key, flags and attributes on message publishing. + * [BC BREAK] Removed publishing with a `routing_key` option from queue configuration, for + AMQP. Use exchange `default_publish_routing_key` or `AmqpStamp` instead. + * [BC BREAK] Changed the `queue` option in the AMQP transport DSN to be `queues[name]`. You can + therefore name the queue but also configure `binding_keys`, `flags` and `arguments`. + * [BC BREAK] The methods `get`, `ack`, `nack` and `queue` of the AMQP `Connection` + have a new argument: the queue name. + * Added optional parameter `prefetch_count` in connection configuration, + to setup channel prefetch count. + * New classes: `RoutableMessageBus`, `AddBusNameStampMiddleware` + and `BusNameStamp` were added, which allow you to add a bus identifier + to the `Envelope` then find the correct bus when receiving from + the transport. See `ConsumeMessagesCommand`. + * The optional `$busNames` constructor argument of the class `ConsumeMessagesCommand` was removed. + * [BC BREAK] 3 new methods were added to `ReceiverInterface`: + `ack()`, `reject()` and `get()`. The methods `receive()` + and `stop()` were removed. + * [BC BREAK] Error handling was moved from the receivers into + `Worker`. Implementations of `ReceiverInterface::handle()` + should now allow all exceptions to be thrown, except for transport + exceptions. They should also not retry (e.g. if there's a queue, + remove from the queue) if there is a problem decoding the message. + * [BC BREAK] `RejectMessageExceptionInterface` was removed and replaced + by `Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException`, + which has the same behavior: a message will not be retried + * The default command name for `ConsumeMessagesCommand` was + changed from `messenger:consume-messages` to `messenger:consume` + * `ConsumeMessagesCommand` has two new optional constructor arguments + * [BC BREAK] The first argument to Worker changed from a single + `ReceiverInterface` to an array of `ReceiverInterface`. + * `Worker` has 3 new optional constructor arguments. + * The `Worker` class now handles calling `pcntl_signal_dispatch()` the + receiver no longer needs to call this. + * The `AmqpSender` will now retry messages using a dead-letter exchange + and delayed queues, instead of retrying via `nack()` + * Senders now receive the `Envelope` with the `SentStamp` on it. Previously, + the `Envelope` was passed to the sender and *then* the `SentStamp` + was added. + * `SerializerInterface` implementations should now throw a + `Symfony\Component\Messenger\Exception\MessageDecodingFailedException` + if `decode()` fails for any reason. + * [BC BREAK] The default `Serializer` will now throw a + `MessageDecodingFailedException` if `decode()` fails, instead + of the underlying exceptions from the Serializer component. + * Added `PhpSerializer` which uses PHP's native `serialize()` and + `unserialize()` to serialize messages to a transport + * [BC BREAK] If no serializer were passed, the default serializer + changed from `Serializer` to `PhpSerializer` inside `AmqpReceiver`, + `AmqpSender`, `AmqpTransport` and `AmqpTransportFactory`. + * Added `TransportException` to mark an exception transport-related + * [BC BREAK] If listening to exceptions while using `AmqpSender` or `AmqpReceiver`, `\AMQPException` is + no longer thrown in favor of `TransportException`. + * Deprecated `LoggingMiddleware`, pass a logger to `SendMessageMiddleware` instead. + * [BC BREAK] `Connection::__construct()` and `Connection::fromDsn()` + both no longer have `$isDebug` arguments. + * [BC BREAK] The Amqp Transport now automatically sets up the exchanges + and queues by default. Previously, this was done when in "debug" mode + only. Pass the `auto_setup` connection option to control this. + * Added a `SetupTransportsCommand` command to setup the transports + * Added a Doctrine transport. For example, use the `doctrine://default` DSN (this uses the `default` Doctrine entity manager) + * [BC BREAK] The `getConnectionConfiguration` method on Amqp's `Connection` has been removed. + * [BC BREAK] A `HandlerFailedException` exception will be thrown if one or more handler fails. + * [BC BREAK] The `HandlersLocationInterface::getHandlers` method needs to return `HandlerDescriptor` + instances instead of callables. + * [BC BREAK] The `HandledStamp` stamp has changed: `handlerAlias` has been renamed to `handlerName`, + `getCallableName` has been removed and its constructor only has 2 arguments now. + * [BC BREAK] The `ReceivedStamp` needs to exposes the name of the transport from which the message + has been received. + +4.2.0 +----- + + * Added `HandleTrait` leveraging a message bus instance to return a single + synchronous message handling result + * Added `HandledStamp` & `SentStamp` stamps + * All the changes below are BC BREAKS + * Senders and handlers subscribing to parent interfaces now receive *all* matching messages, wildcard included + * `MessageBusInterface::dispatch()`, `MiddlewareInterface::handle()` and `SenderInterface::send()` return `Envelope` + * `MiddlewareInterface::handle()` now require an `Envelope` as first argument and a `StackInterface` as second + * `EnvelopeAwareInterface` has been removed + * The signature of `Amqp*` classes changed to take a `Connection` as a first argument and an optional + `Serializer` as a second argument. + * `MessageSubscriberInterface::getHandledMessages()` return value has changed. The value of an array item + needs to be an associative array or the method name. + * `StampInterface` replaces `EnvelopeItemInterface` and doesn't extend `Serializable` anymore + * The `ConsumeMessagesCommand` class now takes an instance of `Psr\Container\ContainerInterface` + as first constructor argument + * The `EncoderInterface` and `DecoderInterface` have been replaced by a unified `Symfony\Component\Messenger\Transport\Serialization\SerializerInterface`. + * Renamed `EnvelopeItemInterface` to `StampInterface` + * `Envelope`'s constructor and `with()` method now accept `StampInterface` objects as variadic parameters + * Renamed and moved `ReceivedMessage`, `ValidationConfiguration` and `SerializerConfiguration` in the `Stamp` namespace + * Removed the `WrapIntoReceivedMessage` class + * `MessengerDataCollector::getMessages()` returns an iterable, not just an array anymore + * `HandlerLocatorInterface::resolve()` has been removed, use `HandlersLocator::getHandlers()` instead + * `SenderLocatorInterface::getSenderForMessage()` has been removed, use `SendersLocator::getSenders()` instead + * Classes in the `Middleware\Enhancers` sub-namespace have been moved to the `Middleware` one + * Classes in the `Asynchronous\Routing` sub-namespace have been moved to the `Transport\Sender\Locator` sub-namespace + * The `Asynchronous/Middleware/SendMessageMiddleware` class has been moved to the `Middleware` namespace + * `SenderInterface` has been moved to the `Transport\Sender` sub-namespace + * The `ChainHandler` and `ChainSender` classes have been removed + * `ReceiverInterface` and its implementations have been moved to the `Transport\Receiver` sub-namespace + * `ActivationMiddlewareDecorator` has been renamed `ActivationMiddleware` + * `AllowNoHandlerMiddleware` has been removed in favor of a new constructor argument on `HandleMessageMiddleware` + * The `ContainerHandlerLocator`, `AbstractHandlerLocator`, `SenderLocator` and `AbstractSenderLocator` classes have been removed + * `Envelope::all()` takes a new optional `$stampFqcn` argument and returns the stamps for the specified FQCN, or all stamps by their class name + * `Envelope::get()` has been renamed `Envelope::last()` + +4.1.0 +----- + + * Introduced the component as experimental diff --git a/plugins/email/vendor/symfony/messenger/Command/AbstractFailedMessagesCommand.php b/plugins/email/vendor/symfony/messenger/Command/AbstractFailedMessagesCommand.php new file mode 100644 index 0000000..7a6fc21 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/AbstractFailedMessagesCommand.php @@ -0,0 +1,298 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Helper\Dumper; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; +use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; +use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; +use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Caster\TraceStub; +use Symfony\Component\VarDumper\Cloner\ClonerInterface; +use Symfony\Component\VarDumper\Cloner\Stub; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Contracts\Service\ServiceProviderInterface; + +/** + * @author Ryan Weaver + * + * @internal + */ +abstract class AbstractFailedMessagesCommand extends Command +{ + protected const DEFAULT_TRANSPORT_OPTION = 'choose'; + + protected $failureTransports; + + private $globalFailureReceiverName; + + /** + * @param ServiceProviderInterface $failureTransports + */ + public function __construct(?string $globalFailureReceiverName, $failureTransports) + { + $this->failureTransports = $failureTransports; + if (!$failureTransports instanceof ServiceProviderInterface) { + trigger_deprecation('symfony/messenger', '5.3', 'Passing a receiver as 2nd argument to "%s()" is deprecated, pass a service locator instead.', __METHOD__); + + if (null === $globalFailureReceiverName) { + throw new InvalidArgumentException(sprintf('The argument "globalFailureReceiver" from method "%s()" must be not null if 2nd argument is not a ServiceLocator.', __METHOD__)); + } + + $this->failureTransports = new ServiceLocator([$globalFailureReceiverName => static function () use ($failureTransports) { return $failureTransports; }]); + } + $this->globalFailureReceiverName = $globalFailureReceiverName; + + parent::__construct(); + } + + protected function getReceiverName(): string + { + trigger_deprecation('symfony/messenger', '5.3', 'The method "%s()" is deprecated, use getGlobalFailureReceiverName() instead.', __METHOD__); + + return $this->globalFailureReceiverName; + } + + protected function getGlobalFailureReceiverName(): ?string + { + return $this->globalFailureReceiverName; + } + + /** + * @return mixed + */ + protected function getMessageId(Envelope $envelope) + { + /** @var TransportMessageIdStamp $stamp */ + $stamp = $envelope->last(TransportMessageIdStamp::class); + + return null !== $stamp ? $stamp->getId() : null; + } + + protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io) + { + $io->title('Failed Message Details'); + + /** @var SentToFailureTransportStamp|null $sentToFailureTransportStamp */ + $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); + /** @var RedeliveryStamp|null $lastRedeliveryStamp */ + $lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class); + /** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */ + $lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class); + $lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true); + + $rows = [ + ['Class', \get_class($envelope->getMessage())], + ]; + + if (null !== $id = $this->getMessageId($envelope)) { + $rows[] = ['Message Id', $id]; + } + + if (null === $sentToFailureTransportStamp) { + $io->warning('Message does not appear to have been sent to this transport after failing'); + } else { + $failedAt = ''; + $errorMessage = ''; + $errorCode = ''; + $errorClass = '(unknown)'; + + if (null !== $lastRedeliveryStamp) { + $failedAt = $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'); + } + + if (null !== $lastErrorDetailsStamp) { + $errorMessage = $lastErrorDetailsStamp->getExceptionMessage(); + $errorCode = $lastErrorDetailsStamp->getExceptionCode(); + $errorClass = $lastErrorDetailsStamp->getExceptionClass(); + } elseif (null !== $lastRedeliveryStampWithException) { + // Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps. + $errorMessage = $lastRedeliveryStampWithException->getExceptionMessage(); + if (null !== $lastRedeliveryStampWithException->getFlattenException()) { + $errorClass = $lastRedeliveryStampWithException->getFlattenException()->getClass(); + } + } + + $rows = array_merge($rows, [ + ['Failed at', $failedAt], + ['Error', $errorMessage], + ['Error Code', $errorCode], + ['Error Class', $errorClass], + ['Transport', $sentToFailureTransportStamp->getOriginalReceiverName()], + ]); + } + + $io->table([], $rows); + + /** @var RedeliveryStamp[] $redeliveryStamps */ + $redeliveryStamps = $envelope->all(RedeliveryStamp::class); + $io->writeln(' Message history:'); + foreach ($redeliveryStamps as $redeliveryStamp) { + $io->writeln(sprintf(' * Message failed at %s and was redelivered', $redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'))); + } + $io->newLine(); + + if ($io->isVeryVerbose()) { + $io->title('Message:'); + $dump = new Dumper($io, null, $this->createCloner()); + $io->writeln($dump($envelope->getMessage())); + $io->title('Exception:'); + $flattenException = null; + if (null !== $lastErrorDetailsStamp) { + $flattenException = $lastErrorDetailsStamp->getFlattenException(); + } elseif (null !== $lastRedeliveryStampWithException) { + $flattenException = $lastRedeliveryStampWithException->getFlattenException(); + } + $io->writeln(null === $flattenException ? '(no data)' : $dump($flattenException)); + } else { + $io->writeln(' Re-run command with -vv to see more message & error details.'); + } + } + + protected function printPendingMessagesMessage(ReceiverInterface $receiver, SymfonyStyle $io) + { + if ($receiver instanceof MessageCountAwareInterface) { + if (1 === $receiver->getMessageCount()) { + $io->writeln('There is 1 message pending in the failure transport.'); + } else { + $io->writeln(sprintf('There are %d messages pending in the failure transport.', $receiver->getMessageCount())); + } + } + } + + /** + * @param string|null $name + */ + protected function getReceiver(/* string $name = null */): ReceiverInterface + { + if (1 > \func_num_args() && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { + trigger_deprecation('symfony/messenger', '5.3', 'The "%s()" method will have a new "string $name" argument in version 6.0, not defining it is deprecated.', __METHOD__); + } + $name = \func_num_args() > 0 ? func_get_arg(0) : null; + + if (null === $name = $name ?? $this->globalFailureReceiverName) { + throw new InvalidArgumentException(sprintf('No default failure transport is defined. Available transports are: "%s".', implode('", "', array_keys($this->failureTransports->getProvidedServices())))); + } + + if (!$this->failureTransports->has($name)) { + throw new InvalidArgumentException(sprintf('The "%s" failure transport was not found. Available transports are: "%s".', $name, implode('", "', array_keys($this->failureTransports->getProvidedServices())))); + } + + return $this->failureTransports->get($name); + } + + protected function getLastRedeliveryStampWithException(Envelope $envelope): ?RedeliveryStamp + { + if (null === \func_get_args()[1]) { + trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "getLastRedeliveryStampWithException" method in the "%s" class is deprecated, use the "Envelope::last(%s)" instead.', self::class, ErrorDetailsStamp::class)); + } + + // Use ErrorDetailsStamp instead if it is available + if (null !== $envelope->last(ErrorDetailsStamp::class)) { + return null; + } + + /** @var RedeliveryStamp $stamp */ + foreach (array_reverse($envelope->all(RedeliveryStamp::class)) as $stamp) { + if (null !== $stamp->getExceptionMessage()) { + return $stamp; + } + } + + return null; + } + + private function createCloner(): ?ClonerInterface + { + if (!class_exists(VarCloner::class)) { + return null; + } + + $cloner = new VarCloner(); + $cloner->addCasters([FlattenException::class => function (FlattenException $flattenException, array $a, Stub $stub): array { + $stub->class = $flattenException->getClass(); + + return [ + Caster::PREFIX_VIRTUAL.'message' => $flattenException->getMessage(), + Caster::PREFIX_VIRTUAL.'code' => $flattenException->getCode(), + Caster::PREFIX_VIRTUAL.'file' => $flattenException->getFile(), + Caster::PREFIX_VIRTUAL.'line' => $flattenException->getLine(), + Caster::PREFIX_VIRTUAL.'trace' => new TraceStub($flattenException->getTrace()), + ]; + }]); + + return $cloner; + } + + protected function printWarningAvailableFailureTransports(SymfonyStyle $io, ?string $failureTransportName): void + { + $failureTransports = array_keys($this->failureTransports->getProvidedServices()); + $failureTransportsCount = \count($failureTransports); + if ($failureTransportsCount > 1) { + $io->writeln([ + sprintf('> Loading messages from the global failure transport %s.', $failureTransportName), + '> To use a different failure transport, pass --transport=.', + sprintf('> Available failure transports are: %s', implode(', ', $failureTransports)), + "\n", + ]); + } + } + + protected function interactiveChooseFailureTransport(SymfonyStyle $io) + { + $failedTransports = array_keys($this->failureTransports->getProvidedServices()); + $question = new ChoiceQuestion('Select failed transport:', $failedTransports, 0); + $question->setMultiselect(false); + + return $io->askQuestion($question); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('transport')) { + $suggestions->suggestValues(array_keys($this->failureTransports->getProvidedServices())); + + return; + } + + if ($input->mustSuggestArgumentValuesFor('id')) { + $transport = $input->getOption('transport'); + $transport = self::DEFAULT_TRANSPORT_OPTION === $transport ? $this->getGlobalFailureReceiverName() : $transport; + $receiver = $this->getReceiver($transport); + + if (!$receiver instanceof ListableReceiverInterface) { + return; + } + + $ids = []; + foreach ($receiver->all(50) as $envelope) { + $ids[] = $this->getMessageId($envelope); + } + $suggestions->suggestValues($ids); + + return; + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/ConsumeMessagesCommand.php b/plugins/email/vendor/symfony/messenger/Command/ConsumeMessagesCommand.php new file mode 100644 index 0000000..6e9a02b --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/ConsumeMessagesCommand.php @@ -0,0 +1,267 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Messenger\EventListener\ResetServicesListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnFailureLimitListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnMemoryLimitListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener; +use Symfony\Component\Messenger\RoutableMessageBus; +use Symfony\Component\Messenger\Worker; + +/** + * @author Samuel Roze + */ +class ConsumeMessagesCommand extends Command +{ + protected static $defaultName = 'messenger:consume'; + protected static $defaultDescription = 'Consume messages'; + + private $routableBus; + private $receiverLocator; + private $eventDispatcher; + private $logger; + private $receiverNames; + private $resetServicesListener; + private $busIds; + + public function __construct(RoutableMessageBus $routableBus, ContainerInterface $receiverLocator, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null, array $receiverNames = [], ResetServicesListener $resetServicesListener = null, array $busIds = []) + { + $this->routableBus = $routableBus; + $this->receiverLocator = $receiverLocator; + $this->eventDispatcher = $eventDispatcher; + $this->logger = $logger; + $this->receiverNames = $receiverNames; + $this->resetServicesListener = $resetServicesListener; + $this->busIds = $busIds; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $defaultReceiverName = 1 === \count($this->receiverNames) ? current($this->receiverNames) : null; + + $this + ->setDefinition([ + new InputArgument('receivers', InputArgument::IS_ARRAY, 'Names of the receivers/transports to consume in order of priority', $defaultReceiverName ? [$defaultReceiverName] : []), + new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of received messages'), + new InputOption('failure-limit', 'f', InputOption::VALUE_REQUIRED, 'The number of failed messages the worker can consume'), + new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'The memory limit the worker can consume'), + new InputOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'The time limit in seconds the worker can handle new messages'), + new InputOption('sleep', null, InputOption::VALUE_REQUIRED, 'Seconds to sleep before asking for new messages after no messages were found', 1), + new InputOption('bus', 'b', InputOption::VALUE_REQUIRED, 'Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically)'), + new InputOption('queues', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit receivers to only consume from the specified queues'), + new InputOption('no-reset', null, InputOption::VALUE_NONE, 'Do not reset container services after each message'), + ]) + ->setDescription(self::$defaultDescription) + ->setHelp(<<<'EOF' +The %command.name% command consumes messages and dispatches them to the message bus. + + php %command.full_name% + +To receive from multiple transports, pass each name: + + php %command.full_name% receiver1 receiver2 + +Use the --limit option to limit the number of messages received: + + php %command.full_name% --limit=10 + +Use the --failure-limit option to stop the worker when the given number of failed messages is reached: + + php %command.full_name% --failure-limit=2 + +Use the --memory-limit option to stop the worker if it exceeds a given memory usage limit. You can use shorthand byte values [K, M or G]: + + php %command.full_name% --memory-limit=128M + +Use the --time-limit option to stop the worker when the given time limit (in seconds) is reached. +If a message is being handled, the worker will stop after the processing is finished: + + php %command.full_name% --time-limit=3600 + +Use the --bus option to specify the message bus to dispatch received messages +to instead of trying to determine it automatically. This is required if the +messages didn't originate from Messenger: + + php %command.full_name% --bus=event_bus + +Use the --queues option to limit a receiver to only certain queues (only supported by some receivers): + + php %command.full_name% --queues=fasttrack + +Use the --no-reset option to prevent services resetting after each message (may lead to leaking services' state between messages): + + php %command.full_name% --no-reset +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + if ($this->receiverNames && 0 === \count($input->getArgument('receivers'))) { + $io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true); + + $io->writeln('Choose which receivers you want to consume messages from in order of priority.'); + if (\count($this->receiverNames) > 1) { + $io->writeln(sprintf('Hint: to consume from multiple, use a list of their names, e.g. %s', implode(', ', $this->receiverNames))); + } + + $question = new ChoiceQuestion('Select receivers to consume:', $this->receiverNames, 0); + $question->setMultiselect(true); + + $input->setArgument('receivers', $io->askQuestion($question)); + } + + if (0 === \count($input->getArgument('receivers'))) { + throw new RuntimeException('Please pass at least one receiver.'); + } + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $receivers = []; + foreach ($receiverNames = $input->getArgument('receivers') as $receiverName) { + if (!$this->receiverLocator->has($receiverName)) { + $message = sprintf('The receiver "%s" does not exist.', $receiverName); + if ($this->receiverNames) { + $message .= sprintf(' Valid receivers are: %s.', implode(', ', $this->receiverNames)); + } + + throw new RuntimeException($message); + } + + $receivers[$receiverName] = $this->receiverLocator->get($receiverName); + } + + if (null !== $this->resetServicesListener && !$input->getOption('no-reset')) { + $this->eventDispatcher->addSubscriber($this->resetServicesListener); + } + + $stopsWhen = []; + if ($limit = $input->getOption('limit')) { + $stopsWhen[] = "processed {$limit} messages"; + $this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener($limit, $this->logger)); + } + + if ($failureLimit = $input->getOption('failure-limit')) { + $stopsWhen[] = "reached {$failureLimit} failed messages"; + $this->eventDispatcher->addSubscriber(new StopWorkerOnFailureLimitListener($failureLimit, $this->logger)); + } + + if ($memoryLimit = $input->getOption('memory-limit')) { + $stopsWhen[] = "exceeded {$memoryLimit} of memory"; + $this->eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener($this->convertToBytes($memoryLimit), $this->logger)); + } + + if (null !== ($timeLimit = $input->getOption('time-limit'))) { + $stopsWhen[] = "been running for {$timeLimit}s"; + $this->eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $this->logger)); + } + + $stopsWhen[] = 'received a stop signal via the messenger:stop-workers command'; + + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + $io->success(sprintf('Consuming messages from transport%s "%s".', \count($receivers) > 0 ? 's' : '', implode(', ', $receiverNames))); + + if ($stopsWhen) { + $last = array_pop($stopsWhen); + $stopsWhen = ($stopsWhen ? implode(', ', $stopsWhen).' or ' : '').$last; + $io->comment("The worker will automatically exit once it has {$stopsWhen}."); + } + + $io->comment('Quit the worker with CONTROL-C.'); + + if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) { + $io->comment('Re-run the command with a -vv option to see logs about consumed messages.'); + } + + $bus = $input->getOption('bus') ? $this->routableBus->getMessageBus($input->getOption('bus')) : $this->routableBus; + + $worker = new Worker($receivers, $bus, $this->eventDispatcher, $this->logger); + $options = [ + 'sleep' => $input->getOption('sleep') * 1000000, + ]; + if ($queues = $input->getOption('queues')) { + $options['queues'] = $queues; + } + $worker->run($options); + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('receivers')) { + $suggestions->suggestValues(array_diff($this->receiverNames, array_diff($input->getArgument('receivers'), [$input->getCompletionValue()]))); + + return; + } + + if ($input->mustSuggestOptionValuesFor('bus')) { + $suggestions->suggestValues($this->busIds); + } + } + + private function convertToBytes(string $memoryLimit): int + { + $memoryLimit = strtolower($memoryLimit); + $max = ltrim($memoryLimit, '+'); + if (str_starts_with($max, '0x')) { + $max = \intval($max, 16); + } elseif (str_starts_with($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr(rtrim($memoryLimit, 'b'), -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + + return $max; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/DebugCommand.php b/plugins/email/vendor/symfony/messenger/Command/DebugCommand.php new file mode 100644 index 0000000..4320ad7 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/DebugCommand.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * A console command to debug Messenger information. + * + * @author Roland Franssen + */ +class DebugCommand extends Command +{ + protected static $defaultName = 'debug:messenger'; + protected static $defaultDescription = 'List messages you can dispatch using the message buses'; + + private $mapping; + + public function __construct(array $mapping) + { + $this->mapping = $mapping; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->addArgument('bus', InputArgument::OPTIONAL, sprintf('The bus id (one of "%s")', implode('", "', array_keys($this->mapping)))) + ->setDescription(self::$defaultDescription) + ->setHelp(<<<'EOF' +The %command.name% command displays all messages that can be +dispatched using the message buses: + + php %command.full_name% + +Or for a specific bus only: + + php %command.full_name% command_bus + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $io->title('Messenger'); + + $mapping = $this->mapping; + if ($bus = $input->getArgument('bus')) { + if (!isset($mapping[$bus])) { + throw new RuntimeException(sprintf('Bus "%s" does not exist. Known buses are "%s".', $bus, implode('", "', array_keys($this->mapping)))); + } + $mapping = [$bus => $mapping[$bus]]; + } + + foreach ($mapping as $bus => $handlersByMessage) { + $io->section($bus); + + $tableRows = []; + foreach ($handlersByMessage as $message => $handlers) { + if ($description = self::getClassDescription($message)) { + $tableRows[] = [sprintf('%s', $description)]; + } + + $tableRows[] = [sprintf('%s', $message)]; + foreach ($handlers as $handler) { + $tableRows[] = [ + sprintf(' handled by %s', $handler[0]).$this->formatConditions($handler[1]), + ]; + if ($handlerDescription = self::getClassDescription($handler[0])) { + $tableRows[] = [sprintf(' %s', $handlerDescription)]; + } + } + $tableRows[] = ['']; + } + + if ($tableRows) { + $io->text('The following messages can be dispatched:'); + $io->newLine(); + $io->table([], $tableRows); + } else { + $io->warning(sprintf('No handled message found in bus "%s".', $bus)); + } + } + + return 0; + } + + private function formatConditions(array $options): string + { + if (!$options) { + return ''; + } + + $optionsMapping = []; + foreach ($options as $key => $value) { + $optionsMapping[] = $key.'='.$value; + } + + return ' (when '.implode(', ', $optionsMapping).')'; + } + + private static function getClassDescription(string $class): string + { + try { + $r = new \ReflectionClass($class); + + if ($docComment = $r->getDocComment()) { + $docComment = preg_split('#\n\s*\*\s*[\n@]#', substr($docComment, 3, -2), 2)[0]; + + return trim(preg_replace('#\s*\n\s*\*\s*#', ' ', $docComment)); + } + } catch (\ReflectionException $e) { + } + + return ''; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('bus')) { + $suggestions->suggestValues(array_keys($this->mapping)); + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/FailedMessagesRemoveCommand.php b/plugins/email/vendor/symfony/messenger/Command/FailedMessagesRemoveCommand.php new file mode 100644 index 0000000..cfdcba5 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/FailedMessagesRemoveCommand.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; + +/** + * @author Ryan Weaver + */ +class FailedMessagesRemoveCommand extends AbstractFailedMessagesCommand +{ + protected static $defaultName = 'messenger:failed:remove'; + protected static $defaultDescription = 'Remove given messages from the failure transport'; + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('id', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'), + new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'), + new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION), + new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'), + ]) + ->setDescription(self::$defaultDescription) + ->setHelp(<<<'EOF' +The %command.name% removes given messages that are pending in the failure transport. + + php %command.full_name% {id1} [{id2} ...] + +The specific ids can be found via the messenger:failed:show command. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $failureTransportName = $input->getOption('transport'); + if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) { + $failureTransportName = $this->getGlobalFailureReceiverName(); + } + + $receiver = $this->getReceiver($failureTransportName); + + $shouldForce = $input->getOption('force'); + $ids = (array) $input->getArgument('id'); + $shouldDisplayMessages = $input->getOption('show-messages') || 1 === \count($ids); + $this->removeMessages($failureTransportName, $ids, $receiver, $io, $shouldForce, $shouldDisplayMessages); + + return 0; + } + + private function removeMessages(string $failureTransportName, array $ids, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void + { + if (!$receiver instanceof ListableReceiverInterface) { + throw new RuntimeException(sprintf('The "%s" receiver does not support removing specific messages.', $failureTransportName)); + } + + foreach ($ids as $id) { + $envelope = $receiver->find($id); + if (null === $envelope) { + $io->error(sprintf('The message with id "%s" was not found.', $id)); + continue; + } + + if ($shouldDisplayMessages) { + $this->displaySingleMessage($envelope, $io); + } + + if ($shouldForce || $io->confirm('Do you want to permanently remove this message?', false)) { + $receiver->reject($envelope); + + $io->success(sprintf('Message with id %s removed.', $id)); + } else { + $io->note(sprintf('Message with id %s not removed.', $id)); + } + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/FailedMessagesRetryCommand.php b/plugins/email/vendor/symfony/messenger/Command/FailedMessagesRetryCommand.php new file mode 100644 index 0000000..0ef9984 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/FailedMessagesRetryCommand.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; +use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\SingleMessageReceiver; +use Symfony\Component\Messenger\Worker; + +/** + * @author Ryan Weaver + */ +class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand +{ + protected static $defaultName = 'messenger:failed:retry'; + protected static $defaultDescription = 'Retry one or more messages from the failure transport'; + + private $eventDispatcher; + private $messageBus; + private $logger; + + public function __construct(?string $globalReceiverName, $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null) + { + $this->eventDispatcher = $eventDispatcher; + $this->messageBus = $messageBus; + $this->logger = $logger; + + parent::__construct($globalReceiverName, $failureTransports); + } + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('id', InputArgument::IS_ARRAY, 'Specific message id(s) to retry'), + new InputOption('force', null, InputOption::VALUE_NONE, 'Force action without confirmation'), + new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION), + ]) + ->setDescription(self::$defaultDescription) + ->setHelp(<<<'EOF' +The %command.name% retries message in the failure transport. + + php %command.full_name% + +The command will interactively ask if each message should be retried +or discarded. + +Some transports support retrying a specific message id, which comes +from the messenger:failed:show command. + + php %command.full_name% {id} + +Or pass multiple ids at once to process multiple messages: + +php %command.full_name% {id1} {id2} {id3} + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1)); + + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + $io->comment('Quit this command with CONTROL-C.'); + if (!$output->isVeryVerbose()) { + $io->comment('Re-run the command with a -vv option to see logs about consumed messages.'); + } + + $failureTransportName = $input->getOption('transport'); + if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) { + $this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName()); + } + if ('' === $failureTransportName || null === $failureTransportName) { + $failureTransportName = $this->interactiveChooseFailureTransport($io); + } + $failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName; + + $receiver = $this->getReceiver($failureTransportName); + $this->printPendingMessagesMessage($receiver, $io); + + $io->writeln(sprintf('To retry all the messages, run messenger:consume %s', $failureTransportName)); + + $shouldForce = $input->getOption('force'); + $ids = $input->getArgument('id'); + if (0 === \count($ids)) { + if (!$input->isInteractive()) { + throw new RuntimeException('Message id must be passed when in non-interactive mode.'); + } + + $this->runInteractive($failureTransportName, $io, $shouldForce); + + return 0; + } + + $this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce); + $io->success('All done!'); + + return 0; + } + + private function runInteractive(string $failureTransportName, SymfonyStyle $io, bool $shouldForce) + { + $receiver = $this->failureTransports->get($failureTransportName); + $count = 0; + if ($receiver instanceof ListableReceiverInterface) { + // for listable receivers, find the messages one-by-one + // this avoids using get(), which for some less-robust + // transports (like Doctrine), will cause the message + // to be temporarily "acked", even if the user aborts + // handling the message + while (true) { + $ids = []; + foreach ($receiver->all(1) as $envelope) { + ++$count; + + $id = $this->getMessageId($envelope); + if (null === $id) { + throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $failureTransportName)); + } + $ids[] = $id; + } + + // break the loop if all messages are consumed + if (0 === \count($ids)) { + break; + } + + $this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce); + } + } else { + // get() and ask messages one-by-one + $count = $this->runWorker($failureTransportName, $receiver, $io, $shouldForce); + } + + // avoid success message if nothing was processed + if (1 <= $count) { + $io->success('All failed messages have been handled or removed!'); + } + } + + private function runWorker(string $failureTransportName, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int + { + $count = 0; + $listener = function (WorkerMessageReceivedEvent $messageReceivedEvent) use ($io, $receiver, $shouldForce, &$count) { + ++$count; + $envelope = $messageReceivedEvent->getEnvelope(); + + $this->displaySingleMessage($envelope, $io); + + $shouldHandle = $shouldForce || $io->confirm('Do you want to retry (yes) or delete this message (no)?'); + + if ($shouldHandle) { + return; + } + + $messageReceivedEvent->shouldHandle(false); + $receiver->reject($envelope); + }; + $this->eventDispatcher->addListener(WorkerMessageReceivedEvent::class, $listener); + + $worker = new Worker( + [$failureTransportName => $receiver], + $this->messageBus, + $this->eventDispatcher, + $this->logger + ); + + try { + $worker->run(); + } finally { + $this->eventDispatcher->removeListener(WorkerMessageReceivedEvent::class, $listener); + } + + return $count; + } + + private function retrySpecificIds(string $failureTransportName, array $ids, SymfonyStyle $io, bool $shouldForce) + { + $receiver = $this->getReceiver($failureTransportName); + + if (!$receiver instanceof ListableReceiverInterface) { + throw new RuntimeException(sprintf('The "%s" receiver does not support retrying messages by id.', $failureTransportName)); + } + + foreach ($ids as $id) { + $envelope = $receiver->find($id); + if (null === $envelope) { + throw new RuntimeException(sprintf('The message "%s" was not found.', $id)); + } + + $singleReceiver = new SingleMessageReceiver($receiver, $envelope); + $this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce); + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/FailedMessagesShowCommand.php b/plugins/email/vendor/symfony/messenger/Command/FailedMessagesShowCommand.php new file mode 100644 index 0000000..6b78c8e --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/FailedMessagesShowCommand.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; +use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; + +/** + * @author Ryan Weaver + */ +class FailedMessagesShowCommand extends AbstractFailedMessagesCommand +{ + protected static $defaultName = 'messenger:failed:show'; + protected static $defaultDescription = 'Show one or more messages from the failure transport'; + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'), + new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50), + new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION), + ]) + ->setDescription(self::$defaultDescription) + ->setHelp(<<<'EOF' +The %command.name% shows message that are pending in the failure transport. + + php %command.full_name% + +Or look at a specific message by its id: + + php %command.full_name% {id} +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $failureTransportName = $input->getOption('transport'); + if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) { + $this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName()); + } + if ('' === $failureTransportName || null === $failureTransportName) { + $failureTransportName = $this->interactiveChooseFailureTransport($io); + } + $failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName; + + $receiver = $this->getReceiver($failureTransportName); + + $this->printPendingMessagesMessage($receiver, $io); + + if (!$receiver instanceof ListableReceiverInterface) { + throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $failureTransportName)); + } + + if (null === $id = $input->getArgument('id')) { + $this->listMessages($failureTransportName, $io, $input->getOption('max')); + } else { + $this->showMessage($failureTransportName, $id, $io); + } + + return 0; + } + + private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max) + { + /** @var ListableReceiverInterface $receiver */ + $receiver = $this->getReceiver($failedTransportName); + $envelopes = $receiver->all($max); + + $rows = []; + foreach ($envelopes as $envelope) { + /** @var RedeliveryStamp|null $lastRedeliveryStamp */ + $lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class); + /** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */ + $lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class); + $lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true); + + $errorMessage = ''; + if (null !== $lastErrorDetailsStamp) { + $errorMessage = $lastErrorDetailsStamp->getExceptionMessage(); + } elseif (null !== $lastRedeliveryStampWithException) { + // Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps. + $errorMessage = $lastRedeliveryStampWithException->getExceptionMessage(); + } + + $rows[] = [ + $this->getMessageId($envelope), + \get_class($envelope->getMessage()), + null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'), + $errorMessage, + ]; + } + + if (0 === \count($rows)) { + $io->success('No failed messages were found.'); + + return; + } + + $io->table(['Id', 'Class', 'Failed at', 'Error'], $rows); + + if (\count($rows) === $max) { + $io->comment(sprintf('Showing first %d messages.', $max)); + } + + $io->comment(sprintf('Run messenger:failed:show {id} --transport=%s -vv to see message details.', $failedTransportName)); + } + + private function showMessage(?string $failedTransportName, string $id, SymfonyStyle $io) + { + /** @var ListableReceiverInterface $receiver */ + $receiver = $this->getReceiver($failedTransportName); + $envelope = $receiver->find($id); + if (null === $envelope) { + throw new RuntimeException(sprintf('The message "%s" was not found.', $id)); + } + + $this->displaySingleMessage($envelope, $io); + + $io->writeln([ + '', + sprintf(' Run messenger:failed:retry %s --transport=%s to retry this message.', $id, $failedTransportName), + sprintf(' Run messenger:failed:remove %s --transport=%s to delete it.', $id, $failedTransportName), + ]); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/SetupTransportsCommand.php b/plugins/email/vendor/symfony/messenger/Command/SetupTransportsCommand.php new file mode 100644 index 0000000..a5feae3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/SetupTransportsCommand.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Messenger\Transport\SetupableTransportInterface; + +/** + * @author Vincent Touzet + */ +class SetupTransportsCommand extends Command +{ + protected static $defaultName = 'messenger:setup-transports'; + protected static $defaultDescription = 'Prepare the required infrastructure for the transport'; + + private $transportLocator; + private $transportNames; + + public function __construct(ContainerInterface $transportLocator, array $transportNames = []) + { + $this->transportLocator = $transportLocator; + $this->transportNames = $transportNames; + + parent::__construct(); + } + + protected function configure() + { + $this + ->addArgument('transport', InputArgument::OPTIONAL, 'Name of the transport to setup', null) + ->setDescription(self::$defaultDescription) + ->setHelp(<<%command.name% command setups the transports: + + php %command.full_name% + +Or a specific transport only: + + php %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $transportNames = $this->transportNames; + // do we want to set up only one transport? + if ($transport = $input->getArgument('transport')) { + if (!$this->transportLocator->has($transport)) { + throw new \RuntimeException(sprintf('The "%s" transport does not exist.', $transport)); + } + $transportNames = [$transport]; + } + + foreach ($transportNames as $id => $transportName) { + $transport = $this->transportLocator->get($transportName); + if ($transport instanceof SetupableTransportInterface) { + $transport->setup(); + $io->success(sprintf('The "%s" transport was set up successfully.', $transportName)); + } else { + $io->note(sprintf('The "%s" transport does not support setup.', $transportName)); + } + } + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('transport')) { + $suggestions->suggestValues($this->transportNames); + + return; + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Command/StopWorkersCommand.php b/plugins/email/vendor/symfony/messenger/Command/StopWorkersCommand.php new file mode 100644 index 0000000..16fb1a4 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Command/StopWorkersCommand.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; + +/** + * @author Ryan Weaver + */ +class StopWorkersCommand extends Command +{ + protected static $defaultName = 'messenger:stop-workers'; + protected static $defaultDescription = 'Stop workers after their current message'; + + private $restartSignalCachePool; + + public function __construct(CacheItemPoolInterface $restartSignalCachePool) + { + $this->restartSignalCachePool = $restartSignalCachePool; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDefinition([]) + ->setDescription(self::$defaultDescription) + ->setHelp(<<<'EOF' +The %command.name% command sends a signal to stop any messenger:consume processes that are running. + + php %command.full_name% + +Each worker command will finish the message they are currently processing +and then exit. Worker commands are *not* automatically restarted: that +should be handled by a process control system. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $cacheItem = $this->restartSignalCachePool->getItem(StopWorkerOnRestartSignalListener::RESTART_REQUESTED_TIMESTAMP_KEY); + $cacheItem->set(microtime(true)); + $this->restartSignalCachePool->save($cacheItem); + + $io->success('Signal successfully sent to stop any running workers.'); + + return 0; + } +} diff --git a/plugins/email/vendor/symfony/messenger/DataCollector/MessengerDataCollector.php b/plugins/email/vendor/symfony/messenger/DataCollector/MessengerDataCollector.php new file mode 100644 index 0000000..ef1ec4e --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/DataCollector/MessengerDataCollector.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\Messenger\TraceableMessageBus; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @author Samuel Roze + * + * @final + */ +class MessengerDataCollector extends DataCollector implements LateDataCollectorInterface +{ + private $traceableBuses = []; + + public function registerBus(string $name, TraceableMessageBus $bus) + { + $this->traceableBuses[$name] = $bus; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + // Noop. Everything is collected live by the traceable buses & cloned as late as possible. + } + + /** + * {@inheritdoc} + */ + public function lateCollect() + { + $this->data = ['messages' => [], 'buses' => array_keys($this->traceableBuses)]; + + $messages = []; + foreach ($this->traceableBuses as $busName => $bus) { + foreach ($bus->getDispatchedMessages() as $message) { + $debugRepresentation = $this->cloneVar($this->collectMessage($busName, $message)); + $messages[] = [$debugRepresentation, $message['callTime']]; + } + } + + // Order by call time + usort($messages, function ($a, $b) { return $a[1] <=> $b[1]; }); + + // Keep the messages clones only + $this->data['messages'] = array_column($messages, 0); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'messenger'; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->data = []; + foreach ($this->traceableBuses as $traceableBus) { + $traceableBus->reset(); + } + } + + /** + * {@inheritdoc} + */ + protected function getCasters(): array + { + $casters = parent::getCasters(); + + // Unset the default caster truncating collectors data. + unset($casters['*']); + + return $casters; + } + + private function collectMessage(string $busName, array $tracedMessage) + { + $message = $tracedMessage['message']; + + $debugRepresentation = [ + 'bus' => $busName, + 'stamps' => $tracedMessage['stamps'] ?? null, + 'stamps_after_dispatch' => $tracedMessage['stamps_after_dispatch'] ?? null, + 'message' => [ + 'type' => new ClassStub(\get_class($message)), + 'value' => $message, + ], + 'caller' => $tracedMessage['caller'], + ]; + + if (isset($tracedMessage['exception'])) { + $exception = $tracedMessage['exception']; + + $debugRepresentation['exception'] = [ + 'type' => \get_class($exception), + 'value' => $exception, + ]; + } + + return $debugRepresentation; + } + + public function getExceptionsCount(string $bus = null): int + { + $count = 0; + foreach ($this->getMessages($bus) as $message) { + $count += (int) isset($message['exception']); + } + + return $count; + } + + public function getMessages(string $bus = null): array + { + if (null === $bus) { + return $this->data['messages']; + } + + return array_filter($this->data['messages'], function ($message) use ($bus) { + return $bus === $message['bus']; + }); + } + + public function getBuses(): array + { + return $this->data['buses']; + } +} diff --git a/plugins/email/vendor/symfony/messenger/DependencyInjection/MessengerPass.php b/plugins/email/vendor/symfony/messenger/DependencyInjection/MessengerPass.php new file mode 100644 index 0000000..f423889 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/DependencyInjection/MessengerPass.php @@ -0,0 +1,402 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Messenger\Handler\HandlerDescriptor; +use Symfony\Component\Messenger\Handler\HandlersLocator; +use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; +use Symfony\Component\Messenger\TraceableMessageBus; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; + +/** + * @author Samuel Roze + */ +class MessengerPass implements CompilerPassInterface +{ + private $handlerTag; + private $busTag; + private $receiverTag; + + public function __construct(string $handlerTag = 'messenger.message_handler', string $busTag = 'messenger.bus', string $receiverTag = 'messenger.receiver') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/messenger', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->handlerTag = $handlerTag; + $this->busTag = $busTag; + $this->receiverTag = $receiverTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $busIds = []; + foreach ($container->findTaggedServiceIds($this->busTag) as $busId => $tags) { + $busIds[] = $busId; + if ($container->hasParameter($busMiddlewareParameter = $busId.'.middleware')) { + $this->registerBusMiddleware($container, $busId, $container->getParameter($busMiddlewareParameter)); + + $container->getParameterBag()->remove($busMiddlewareParameter); + } + + if ($container->hasDefinition('data_collector.messenger')) { + $this->registerBusToCollector($container, $busId); + } + } + + if ($container->hasDefinition('messenger.receiver_locator')) { + $this->registerReceivers($container, $busIds); + } + $this->registerHandlers($container, $busIds); + } + + private function registerHandlers(ContainerBuilder $container, array $busIds) + { + $definitions = []; + $handlersByBusAndMessage = []; + $handlerToOriginalServiceIdMapping = []; + + foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) { + foreach ($tags as $tag) { + if (isset($tag['bus']) && !\in_array($tag['bus'], $busIds, true)) { + throw new RuntimeException(sprintf('Invalid handler service "%s": bus "%s" specified on the tag "%s" does not exist (known ones are: "%s").', $serviceId, $tag['bus'], $this->handlerTag, implode('", "', $busIds))); + } + + $className = $this->getServiceClass($container, $serviceId); + $r = $container->getReflectionClass($className); + + if (null === $r) { + throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $serviceId, $className)); + } + + if (isset($tag['handles'])) { + $handles = isset($tag['method']) ? [$tag['handles'] => $tag['method']] : [$tag['handles']]; + } else { + $handles = $this->guessHandledClasses($r, $serviceId); + } + + $message = null; + $handlerBuses = (array) ($tag['bus'] ?? $busIds); + + foreach ($handles as $message => $options) { + $buses = $handlerBuses; + + if (\is_int($message)) { + if (\is_string($options)) { + $message = $options; + $options = []; + } else { + throw new RuntimeException(sprintf('The handler configuration needs to return an array of messages or an associated array of message and configuration. Found value of type "%s" at position "%d" for service "%s".', get_debug_type($options), $message, $serviceId)); + } + } + + if (\is_string($options)) { + $options = ['method' => $options]; + } + + if (!isset($options['from_transport']) && isset($tag['from_transport'])) { + $options['from_transport'] = $tag['from_transport']; + } + + $priority = $tag['priority'] ?? $options['priority'] ?? 0; + $method = $options['method'] ?? '__invoke'; + + if (isset($options['bus'])) { + if (!\in_array($options['bus'], $busIds)) { + $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method)); + + throw new RuntimeException(sprintf('Invalid configuration "%s" for message "%s": bus "%s" does not exist.', $messageLocation, $message, $options['bus'])); + } + + $buses = [$options['bus']]; + } + + if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { + $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method)); + + throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" "%s" not found.', $serviceId, $message, $messageLocation)); + } + + if (!$r->hasMethod($method)) { + throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method)); + } + + if ('__invoke' !== $method) { + $wrapperDefinition = (new Definition('callable'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); + + $definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method)] = $wrapperDefinition; + } else { + $definitionId = $serviceId; + } + + $handlerToOriginalServiceIdMapping[$definitionId] = $serviceId; + + foreach ($buses as $handlerBus) { + $handlersByBusAndMessage[$handlerBus][$message][$priority][] = [$definitionId, $options]; + } + } + + if (null === $message) { + throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::getHandledMessages()" must return one or more messages.', $serviceId, $r->getName())); + } + } + } + + foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) { + foreach ($handlersByMessage as $message => $handlersByPriority) { + krsort($handlersByPriority); + $handlersByBusAndMessage[$bus][$message] = array_merge(...$handlersByPriority); + } + } + + $handlersLocatorMappingByBus = []; + foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) { + foreach ($handlersByMessage as $message => $handlers) { + $handlerDescriptors = []; + foreach ($handlers as $handler) { + $definitions[$definitionId = '.messenger.handler_descriptor.'.ContainerBuilder::hash($bus.':'.$message.':'.$handler[0])] = (new Definition(HandlerDescriptor::class))->setArguments([new Reference($handler[0]), $handler[1]]); + $handlerDescriptors[] = new Reference($definitionId); + } + + $handlersLocatorMappingByBus[$bus][$message] = new IteratorArgument($handlerDescriptors); + } + } + $container->addDefinitions($definitions); + + foreach ($busIds as $bus) { + $container->register($locatorId = $bus.'.messenger.handlers_locator', HandlersLocator::class) + ->setArgument(0, $handlersLocatorMappingByBus[$bus] ?? []) + ; + if ($container->has($handleMessageId = $bus.'.middleware.handle_message')) { + $container->getDefinition($handleMessageId) + ->replaceArgument(0, new Reference($locatorId)) + ; + } + } + + if ($container->hasDefinition('console.command.messenger_debug')) { + $debugCommandMapping = $handlersByBusAndMessage; + foreach ($busIds as $bus) { + if (!isset($debugCommandMapping[$bus])) { + $debugCommandMapping[$bus] = []; + } + + foreach ($debugCommandMapping[$bus] as $message => $handlers) { + foreach ($handlers as $key => $handler) { + $debugCommandMapping[$bus][$message][$key][0] = $handlerToOriginalServiceIdMapping[$handler[0]]; + } + } + } + $container->getDefinition('console.command.messenger_debug')->replaceArgument(0, $debugCommandMapping); + } + } + + private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId): iterable + { + if ($handlerClass->implementsInterface(MessageSubscriberInterface::class)) { + return $handlerClass->getName()::getHandledMessages(); + } + + try { + $method = $handlerClass->getMethod('__invoke'); + } catch (\ReflectionException $e) { + throw new RuntimeException(sprintf('Invalid handler service "%s": class "%s" must have an "__invoke()" method.', $serviceId, $handlerClass->getName())); + } + + if (0 === $method->getNumberOfRequiredParameters()) { + throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::__invoke()" requires at least one argument, first one being the message it handles.', $serviceId, $handlerClass->getName())); + } + + $parameters = $method->getParameters(); + if (!$type = $parameters[0]->getType()) { + throw new RuntimeException(sprintf('Invalid handler service "%s": argument "$%s" of method "%s::__invoke()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName())); + } + + if ($type instanceof \ReflectionUnionType) { + $types = []; + $invalidTypes = []; + foreach ($type->getTypes() as $type) { + if (!$type->isBuiltin()) { + $types[] = (string) $type; + } else { + $invalidTypes[] = (string) $type; + } + } + + if ($types) { + return $types; + } + + throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), implode('|', $invalidTypes))); + } + + if ($type->isBuiltin()) { + throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type)); + } + + return [$type->getName()]; + } + + private function registerReceivers(ContainerBuilder $container, array $busIds) + { + $receiverMapping = []; + $failureTransportsMap = []; + if ($container->hasDefinition('console.command.messenger_failed_messages_retry')) { + $commandDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry'); + $globalReceiverName = $commandDefinition->getArgument(0); + if (null !== $globalReceiverName) { + if ($container->hasAlias('messenger.failure_transports.default')) { + $failureTransportsMap[$globalReceiverName] = new Reference('messenger.failure_transports.default'); + } else { + $failureTransportsMap[$globalReceiverName] = new Reference('messenger.transport.'.$globalReceiverName); + } + } + } + + foreach ($container->findTaggedServiceIds($this->receiverTag) as $id => $tags) { + $receiverClass = $this->getServiceClass($container, $id); + if (!is_subclass_of($receiverClass, ReceiverInterface::class)) { + throw new RuntimeException(sprintf('Invalid receiver "%s": class "%s" must implement interface "%s".', $id, $receiverClass, ReceiverInterface::class)); + } + + $receiverMapping[$id] = new Reference($id); + + foreach ($tags as $tag) { + if (isset($tag['alias'])) { + $receiverMapping[$tag['alias']] = $receiverMapping[$id]; + if ($tag['is_failure_transport'] ?? false) { + $failureTransportsMap[$tag['alias']] = $receiverMapping[$id]; + } + } + } + } + + $receiverNames = []; + foreach ($receiverMapping as $name => $reference) { + $receiverNames[(string) $reference] = $name; + } + + $buses = []; + foreach ($busIds as $busId) { + $buses[$busId] = new Reference($busId); + } + + if ($hasRoutableMessageBus = $container->hasDefinition('messenger.routable_message_bus')) { + $container->getDefinition('messenger.routable_message_bus') + ->replaceArgument(0, ServiceLocatorTagPass::register($container, $buses)); + } + + if ($container->hasDefinition('console.command.messenger_consume_messages')) { + $consumeCommandDefinition = $container->getDefinition('console.command.messenger_consume_messages'); + + if ($hasRoutableMessageBus) { + $consumeCommandDefinition->replaceArgument(0, new Reference('messenger.routable_message_bus')); + } + + $consumeCommandDefinition->replaceArgument(4, array_values($receiverNames)); + try { + $consumeCommandDefinition->replaceArgument(6, $busIds); + } catch (OutOfBoundsException $e) { + // ignore to preserve compatibility with symfony/framework-bundle < 5.4 + } + } + + if ($container->hasDefinition('console.command.messenger_setup_transports')) { + $container->getDefinition('console.command.messenger_setup_transports') + ->replaceArgument(1, array_values($receiverNames)); + } + + $container->getDefinition('messenger.receiver_locator')->replaceArgument(0, $receiverMapping); + + $failureTransportsLocator = ServiceLocatorTagPass::register($container, $failureTransportsMap); + + $failedCommandIds = [ + 'console.command.messenger_failed_messages_retry', + 'console.command.messenger_failed_messages_show', + 'console.command.messenger_failed_messages_remove', + ]; + foreach ($failedCommandIds as $failedCommandId) { + if ($container->hasDefinition($failedCommandId)) { + $definition = $container->getDefinition($failedCommandId); + $definition->replaceArgument(1, $failureTransportsLocator); + } + } + } + + private function registerBusToCollector(ContainerBuilder $container, string $busId) + { + $container->setDefinition( + $tracedBusId = 'debug.traced.'.$busId, + (new Definition(TraceableMessageBus::class, [new Reference($tracedBusId.'.inner')]))->setDecoratedService($busId) + ); + + $container->getDefinition('data_collector.messenger')->addMethodCall('registerBus', [$busId, new Reference($tracedBusId)]); + } + + private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middlewareCollection) + { + $middlewareReferences = []; + foreach ($middlewareCollection as $middlewareItem) { + $id = $middlewareItem['id']; + $arguments = $middlewareItem['arguments'] ?? []; + if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$id)) { + $messengerMiddlewareId = $id; + } + + if (!$container->has($messengerMiddlewareId)) { + throw new RuntimeException(sprintf('Invalid middleware: service "%s" not found.', $id)); + } + + if ($container->findDefinition($messengerMiddlewareId)->isAbstract()) { + $childDefinition = new ChildDefinition($messengerMiddlewareId); + $childDefinition->setArguments($arguments); + if (isset($middlewareReferences[$messengerMiddlewareId = $busId.'.middleware.'.$id])) { + $messengerMiddlewareId .= '.'.ContainerBuilder::hash($arguments); + } + $container->setDefinition($messengerMiddlewareId, $childDefinition); + } elseif ($arguments) { + throw new RuntimeException(sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id)); + } + + $middlewareReferences[$messengerMiddlewareId] = new Reference($messengerMiddlewareId); + } + + $container->getDefinition($busId)->replaceArgument(0, new IteratorArgument(array_values($middlewareReferences))); + } + + private function getServiceClass(ContainerBuilder $container, string $serviceId): string + { + while (true) { + $definition = $container->findDefinition($serviceId); + + if (!$definition->getClass() && $definition instanceof ChildDefinition) { + $serviceId = $definition->getParent(); + + continue; + } + + return $definition->getClass(); + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Envelope.php b/plugins/email/vendor/symfony/messenger/Envelope.php new file mode 100644 index 0000000..49476fe --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Envelope.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Symfony\Component\Messenger\Stamp\StampInterface; + +/** + * A message wrapped in an envelope with stamps (configurations, markers, ...). + * + * @author Maxime Steinhausser + */ +final class Envelope +{ + /** + * @var array> + */ + private $stamps = []; + private $message; + + /** + * @param StampInterface[] $stamps + */ + public function __construct(object $message, array $stamps = []) + { + $this->message = $message; + + foreach ($stamps as $stamp) { + $this->stamps[\get_class($stamp)][] = $stamp; + } + } + + /** + * Makes sure the message is in an Envelope and adds the given stamps. + * + * @param object|Envelope $message + * @param StampInterface[] $stamps + */ + public static function wrap(object $message, array $stamps = []): self + { + $envelope = $message instanceof self ? $message : new self($message); + + return $envelope->with(...$stamps); + } + + /** + * @return static A new Envelope instance with additional stamp + */ + public function with(StampInterface ...$stamps): self + { + $cloned = clone $this; + + foreach ($stamps as $stamp) { + $cloned->stamps[\get_class($stamp)][] = $stamp; + } + + return $cloned; + } + + /** + * @return static A new Envelope instance without any stamps of the given class + */ + public function withoutAll(string $stampFqcn): self + { + $cloned = clone $this; + + unset($cloned->stamps[$this->resolveAlias($stampFqcn)]); + + return $cloned; + } + + /** + * Removes all stamps that implement the given type. + */ + public function withoutStampsOfType(string $type): self + { + $cloned = clone $this; + $type = $this->resolveAlias($type); + + foreach ($cloned->stamps as $class => $stamps) { + if ($class === $type || is_subclass_of($class, $type)) { + unset($cloned->stamps[$class]); + } + } + + return $cloned; + } + + public function last(string $stampFqcn): ?StampInterface + { + return isset($this->stamps[$stampFqcn = $this->resolveAlias($stampFqcn)]) ? end($this->stamps[$stampFqcn]) : null; + } + + /** + * @return StampInterface[]|StampInterface[][] The stamps for the specified FQCN, or all stamps by their class name + */ + public function all(string $stampFqcn = null): array + { + if (null !== $stampFqcn) { + return $this->stamps[$this->resolveAlias($stampFqcn)] ?? []; + } + + return $this->stamps; + } + + /** + * @return object The original message contained in the envelope + */ + public function getMessage(): object + { + return $this->message; + } + + /** + * BC to be removed in 6.0. + */ + private function resolveAlias(string $fqcn): string + { + static $resolved; + + return $resolved[$fqcn] ?? ($resolved[$fqcn] = class_exists($fqcn) ? (new \ReflectionClass($fqcn))->getName() : $fqcn); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/AbstractWorkerMessageEvent.php b/plugins/email/vendor/symfony/messenger/Event/AbstractWorkerMessageEvent.php new file mode 100644 index 0000000..e3ece81 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/AbstractWorkerMessageEvent.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\StampInterface; + +abstract class AbstractWorkerMessageEvent +{ + private $envelope; + private $receiverName; + + public function __construct(Envelope $envelope, string $receiverName) + { + $this->envelope = $envelope; + $this->receiverName = $receiverName; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + /** + * Returns a unique identifier for transport receiver this message was received from. + */ + public function getReceiverName(): string + { + return $this->receiverName; + } + + public function addStamps(StampInterface ...$stamps): void + { + $this->envelope = $this->envelope->with(...$stamps); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/SendMessageToTransportsEvent.php b/plugins/email/vendor/symfony/messenger/Event/SendMessageToTransportsEvent.php new file mode 100644 index 0000000..5fd5fd8 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/SendMessageToTransportsEvent.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +use Symfony\Component\Messenger\Envelope; + +/** + * Event is dispatched before a message is sent to the transport. + * + * The event is *only* dispatched if the message will actually + * be sent to at least one transport. If the message is sent + * to multiple transports, the message is dispatched only one time. + * This message is only dispatched the first time a message + * is sent to a transport, not also if it is retried. + * + * @author Ryan Weaver + */ +final class SendMessageToTransportsEvent +{ + private $envelope; + + public function __construct(Envelope $envelope) + { + $this->envelope = $envelope; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + public function setEnvelope(Envelope $envelope) + { + $this->envelope = $envelope; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerMessageFailedEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageFailedEvent.php new file mode 100644 index 0000000..7fb858c --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageFailedEvent.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +use Symfony\Component\Messenger\Envelope; + +/** + * Dispatched when a message was received from a transport and handling failed. + * + * The event name is the class name. + */ +final class WorkerMessageFailedEvent extends AbstractWorkerMessageEvent +{ + private $throwable; + private $willRetry = false; + + public function __construct(Envelope $envelope, string $receiverName, \Throwable $error) + { + $this->throwable = $error; + + parent::__construct($envelope, $receiverName); + } + + public function getThrowable(): \Throwable + { + return $this->throwable; + } + + public function willRetry(): bool + { + return $this->willRetry; + } + + public function setForRetry(): void + { + $this->willRetry = true; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerMessageHandledEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageHandledEvent.php new file mode 100644 index 0000000..3c4a030 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageHandledEvent.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +/** + * Dispatched after a message was received from a transport and successfully handled. + * + * The event name is the class name. + */ +final class WorkerMessageHandledEvent extends AbstractWorkerMessageEvent +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerMessageReceivedEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageReceivedEvent.php new file mode 100644 index 0000000..5b99edc --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageReceivedEvent.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +/** + * Dispatched when a message was received from a transport but before sent to the bus. + * + * The event name is the class name. + */ +final class WorkerMessageReceivedEvent extends AbstractWorkerMessageEvent +{ + private $shouldHandle = true; + + public function shouldHandle(bool $shouldHandle = null): bool + { + if (null !== $shouldHandle) { + $this->shouldHandle = $shouldHandle; + } + + return $this->shouldHandle; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerMessageRetriedEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageRetriedEvent.php new file mode 100644 index 0000000..d348b44 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerMessageRetriedEvent.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +/** + * Dispatched after a message has been sent for retry. + * + * The event name is the class name. + */ +final class WorkerMessageRetriedEvent extends AbstractWorkerMessageEvent +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerRunningEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerRunningEvent.php new file mode 100644 index 0000000..ca32cb4 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerRunningEvent.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +use Symfony\Component\Messenger\Worker; + +/** + * Dispatched after the worker processed a message or didn't receive a message at all. + * + * @author Tobias Schultze + */ +final class WorkerRunningEvent +{ + private $worker; + private $isWorkerIdle; + + public function __construct(Worker $worker, bool $isWorkerIdle) + { + $this->worker = $worker; + $this->isWorkerIdle = $isWorkerIdle; + } + + public function getWorker(): Worker + { + return $this->worker; + } + + /** + * Returns true when no message has been received by the worker. + */ + public function isWorkerIdle(): bool + { + return $this->isWorkerIdle; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerStartedEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerStartedEvent.php new file mode 100644 index 0000000..9d37d8d --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerStartedEvent.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +use Symfony\Component\Messenger\Worker; + +/** + * Dispatched when a worker has been started. + * + * @author Tobias Schultze + */ +final class WorkerStartedEvent +{ + private $worker; + + public function __construct(Worker $worker) + { + $this->worker = $worker; + } + + public function getWorker(): Worker + { + return $this->worker; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Event/WorkerStoppedEvent.php b/plugins/email/vendor/symfony/messenger/Event/WorkerStoppedEvent.php new file mode 100644 index 0000000..e0d4610 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Event/WorkerStoppedEvent.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Event; + +use Symfony\Component\Messenger\Worker; + +/** + * Dispatched when a worker has been stopped. + * + * @author Robin Chalas + */ +final class WorkerStoppedEvent +{ + private $worker; + + public function __construct(Worker $worker) + { + $this->worker = $worker; + } + + public function getWorker(): Worker + { + return $this->worker; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/AddErrorDetailsStampListener.php b/plugins/email/vendor/symfony/messenger/EventListener/AddErrorDetailsStampListener.php new file mode 100644 index 0000000..fd5c9d6 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/AddErrorDetailsStampListener.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp; + +final class AddErrorDetailsStampListener implements EventSubscriberInterface +{ + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + $stamp = ErrorDetailsStamp::create($event->getThrowable()); + $previousStamp = $event->getEnvelope()->last(ErrorDetailsStamp::class); + + // Do not append duplicate information + if (null === $previousStamp || !$previousStamp->equals($stamp)) { + $event->addStamps($stamp); + } + } + + public static function getSubscribedEvents(): array + { + return [ + // must have higher priority than SendFailedMessageForRetryListener + WorkerMessageFailedEvent::class => ['onMessageFailed', 200], + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/DispatchPcntlSignalListener.php b/plugins/email/vendor/symfony/messenger/EventListener/DispatchPcntlSignalListener.php new file mode 100644 index 0000000..27182bd --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/DispatchPcntlSignalListener.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; + +/** + * @author Tobias Schultze + */ +class DispatchPcntlSignalListener implements EventSubscriberInterface +{ + public function onWorkerRunning(): void + { + pcntl_signal_dispatch(); + } + + public static function getSubscribedEvents() + { + if (!\function_exists('pcntl_signal_dispatch')) { + return []; + } + + return [ + WorkerRunningEvent::class => ['onWorkerRunning', 100], + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/ResetServicesListener.php b/plugins/email/vendor/symfony/messenger/EventListener/ResetServicesListener.php new file mode 100644 index 0000000..d5266f3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/ResetServicesListener.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Event\WorkerStoppedEvent; + +/** + * @author Grégoire Pineau + */ +class ResetServicesListener implements EventSubscriberInterface +{ + private $servicesResetter; + + public function __construct(ServicesResetter $servicesResetter) + { + $this->servicesResetter = $servicesResetter; + } + + public function resetServices(WorkerRunningEvent $event): void + { + if (!$event->isWorkerIdle()) { + $this->servicesResetter->reset(); + } + } + + public function resetServicesAtStop(WorkerStoppedEvent $event): void + { + $this->servicesResetter->reset(); + } + + public static function getSubscribedEvents(): array + { + return [ + WorkerRunningEvent::class => ['resetServices', -1024], + WorkerStoppedEvent::class => ['resetServicesAtStop', -1024], + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageForRetryListener.php b/plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageForRetryListener.php new file mode 100644 index 0000000..dab74b2 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageForRetryListener.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerMessageRetriedEvent; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface; +use Symfony\Component\Messenger\Exception\RuntimeException; +use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface; +use Symfony\Component\Messenger\Retry\RetryStrategyInterface; +use Symfony\Component\Messenger\Stamp\DelayStamp; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; +use Symfony\Component\Messenger\Stamp\StampInterface; +use Symfony\Component\Messenger\Transport\Sender\SenderInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * @author Tobias Schultze + */ +class SendFailedMessageForRetryListener implements EventSubscriberInterface +{ + private $sendersLocator; + private $retryStrategyLocator; + private $logger; + private $eventDispatcher; + private $historySize; + + public function __construct(ContainerInterface $sendersLocator, ContainerInterface $retryStrategyLocator, LoggerInterface $logger = null, EventDispatcherInterface $eventDispatcher = null, int $historySize = 10) + { + $this->sendersLocator = $sendersLocator; + $this->retryStrategyLocator = $retryStrategyLocator; + $this->logger = $logger; + $this->eventDispatcher = $eventDispatcher; + $this->historySize = $historySize; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event) + { + $retryStrategy = $this->getRetryStrategyForTransport($event->getReceiverName()); + $envelope = $event->getEnvelope(); + $throwable = $event->getThrowable(); + + $message = $envelope->getMessage(); + $context = [ + 'class' => \get_class($message), + ]; + + $shouldRetry = $retryStrategy && $this->shouldRetry($throwable, $envelope, $retryStrategy); + + $retryCount = RedeliveryStamp::getRetryCountFromEnvelope($envelope); + if ($shouldRetry) { + $event->setForRetry(); + + ++$retryCount; + + $delay = $retryStrategy->getWaitingTime($envelope, $throwable); + + if (null !== $this->logger) { + $this->logger->warning('Error thrown while handling message {class}. Sending for retry #{retryCount} using {delay} ms delay. Error: "{error}"', $context + ['retryCount' => $retryCount, 'delay' => $delay, 'error' => $throwable->getMessage(), 'exception' => $throwable]); + } + + // add the delay and retry stamp info + $retryEnvelope = $this->withLimitedHistory($envelope, new DelayStamp($delay), new RedeliveryStamp($retryCount)); + + // re-send the message for retry + $this->getSenderForTransport($event->getReceiverName())->send($retryEnvelope); + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new WorkerMessageRetriedEvent($retryEnvelope, $event->getReceiverName())); + } + } else { + if (null !== $this->logger) { + $this->logger->critical('Error thrown while handling message {class}. Removing from transport after {retryCount} retries. Error: "{error}"', $context + ['retryCount' => $retryCount, 'error' => $throwable->getMessage(), 'exception' => $throwable]); + } + } + } + + /** + * Adds stamps to the envelope by keeping only the First + Last N stamps. + */ + private function withLimitedHistory(Envelope $envelope, StampInterface ...$stamps): Envelope + { + foreach ($stamps as $stamp) { + $history = $envelope->all(\get_class($stamp)); + if (\count($history) < $this->historySize) { + $envelope = $envelope->with($stamp); + continue; + } + + $history = array_merge( + [$history[0]], + \array_slice($history, -$this->historySize + 2), + [$stamp] + ); + + $envelope = $envelope->withoutAll(\get_class($stamp))->with(...$history); + } + + return $envelope; + } + + public static function getSubscribedEvents() + { + return [ + // must have higher priority than SendFailedMessageToFailureTransportListener + WorkerMessageFailedEvent::class => ['onMessageFailed', 100], + ]; + } + + private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInterface $retryStrategy): bool + { + if ($e instanceof RecoverableExceptionInterface) { + return true; + } + + // if one or more nested Exceptions is an instance of RecoverableExceptionInterface we should retry + // if ALL nested Exceptions are an instance of UnrecoverableExceptionInterface we should not retry + if ($e instanceof HandlerFailedException) { + $shouldNotRetry = true; + foreach ($e->getNestedExceptions() as $nestedException) { + if ($nestedException instanceof RecoverableExceptionInterface) { + return true; + } + + if (!$nestedException instanceof UnrecoverableExceptionInterface) { + $shouldNotRetry = false; + break; + } + } + if ($shouldNotRetry) { + return false; + } + } + + if ($e instanceof UnrecoverableExceptionInterface) { + return false; + } + + return $retryStrategy->isRetryable($envelope, $e); + } + + private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface + { + if ($this->retryStrategyLocator->has($alias)) { + return $this->retryStrategyLocator->get($alias); + } + + return null; + } + + private function getSenderForTransport(string $alias): SenderInterface + { + if ($this->sendersLocator->has($alias)) { + return $this->sendersLocator->get($alias); + } + + throw new RuntimeException(sprintf('Could not find sender "%s" based on the same receiver to send the failed message to for retry.', $alias)); + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageToFailureTransportListener.php b/plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageToFailureTransportListener.php new file mode 100644 index 0000000..644f5a6 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/SendFailedMessageToFailureTransportListener.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Stamp\DelayStamp; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; +use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; +use Symfony\Component\Messenger\Transport\Sender\SenderInterface; + +/** + * Sends a rejected message to a "failure transport". + * + * @author Ryan Weaver + */ +class SendFailedMessageToFailureTransportListener implements EventSubscriberInterface +{ + private $failureSenders; + private $logger; + + /** + * @param ContainerInterface $failureSenders + */ + public function __construct($failureSenders, LoggerInterface $logger = null) + { + if (!$failureSenders instanceof ContainerInterface) { + trigger_deprecation('symfony/messenger', '5.3', 'Passing a SenderInterface value as 1st argument to "%s()" is deprecated, pass a ServiceLocator instead.', __METHOD__); + } + + $this->failureSenders = $failureSenders; + $this->logger = $logger; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event) + { + if ($event->willRetry()) { + return; + } + + if (!$this->hasFailureTransports($event)) { + return; + } + + $failureSender = $this->getFailureSender($event->getReceiverName()); + if (null === $failureSender) { + return; + } + + $envelope = $event->getEnvelope(); + + // avoid re-sending to the failed sender + if (null !== $envelope->last(SentToFailureTransportStamp::class)) { + return; + } + + $envelope = $envelope->with( + new SentToFailureTransportStamp($event->getReceiverName()), + new DelayStamp(0), + new RedeliveryStamp(0) + ); + + if (null !== $this->logger) { + $this->logger->info('Rejected message {class} will be sent to the failure transport {transport}.', [ + 'class' => \get_class($envelope->getMessage()), + 'transport' => \get_class($failureSender), + ]); + } + + $failureSender->send($envelope); + } + + public static function getSubscribedEvents() + { + return [ + WorkerMessageFailedEvent::class => ['onMessageFailed', -100], + ]; + } + + private function getFailureSender(string $receiverName): SenderInterface + { + if ($this->failureSenders instanceof SenderInterface) { + return $this->failureSenders; + } + + return $this->failureSenders->get($receiverName); + } + + private function hasFailureTransports(WorkerMessageFailedEvent $event): bool + { + return ($this->failureSenders instanceof ContainerInterface && $this->failureSenders->has($event->getReceiverName())) || $this->failureSenders instanceof SenderInterface; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnCustomStopExceptionListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnCustomStopExceptionListener.php new file mode 100644 index 0000000..ee8daec --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnCustomStopExceptionListener.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\StopWorkerExceptionInterface; + +/** + * @author Grégoire Pineau + */ +class StopWorkerOnCustomStopExceptionListener implements EventSubscriberInterface +{ + private $stop = false; + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + $th = $event->getThrowable(); + if ($th instanceof StopWorkerExceptionInterface) { + $this->stop = true; + } + if ($th instanceof HandlerFailedException) { + foreach ($th->getNestedExceptions() as $e) { + if ($e instanceof StopWorkerExceptionInterface) { + $this->stop = true; + break; + } + } + } + } + + public function onWorkerRunning(WorkerRunningEvent $event): void + { + if ($this->stop) { + $event->getWorker()->stop(); + } + } + + public static function getSubscribedEvents(): array + { + return [ + WorkerMessageFailedEvent::class => 'onMessageFailed', + WorkerRunningEvent::class => 'onWorkerRunning', + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnFailureLimitListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnFailureLimitListener.php new file mode 100644 index 0000000..29dc6aa --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnFailureLimitListener.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; + +/** + * @author Michel Hunziker + */ +class StopWorkerOnFailureLimitListener implements EventSubscriberInterface +{ + private $maximumNumberOfFailures; + private $logger; + private $failedMessages = 0; + + public function __construct(int $maximumNumberOfFailures, LoggerInterface $logger = null) + { + $this->maximumNumberOfFailures = $maximumNumberOfFailures; + $this->logger = $logger; + + if ($maximumNumberOfFailures <= 0) { + throw new InvalidArgumentException('Failure limit must be greater than zero.'); + } + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + ++$this->failedMessages; + } + + public function onWorkerRunning(WorkerRunningEvent $event): void + { + if (!$event->isWorkerIdle() && $this->failedMessages >= $this->maximumNumberOfFailures) { + $this->failedMessages = 0; + $event->getWorker()->stop(); + + if (null !== $this->logger) { + $this->logger->info('Worker stopped due to limit of {count} failed message(s) is reached', ['count' => $this->maximumNumberOfFailures]); + } + } + } + + public static function getSubscribedEvents(): array + { + return [ + WorkerMessageFailedEvent::class => 'onMessageFailed', + WorkerRunningEvent::class => 'onWorkerRunning', + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnMemoryLimitListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnMemoryLimitListener.php new file mode 100644 index 0000000..73350fd --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnMemoryLimitListener.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; + +/** + * @author Simon Delicata + * @author Tobias Schultze + */ +class StopWorkerOnMemoryLimitListener implements EventSubscriberInterface +{ + private $memoryLimit; + private $logger; + private $memoryResolver; + + public function __construct(int $memoryLimit, LoggerInterface $logger = null, callable $memoryResolver = null) + { + $this->memoryLimit = $memoryLimit; + $this->logger = $logger; + $this->memoryResolver = $memoryResolver ?: static function () { + return memory_get_usage(true); + }; + } + + public function onWorkerRunning(WorkerRunningEvent $event): void + { + $memoryResolver = $this->memoryResolver; + $usedMemory = $memoryResolver(); + if ($usedMemory > $this->memoryLimit) { + $event->getWorker()->stop(); + if (null !== $this->logger) { + $this->logger->info('Worker stopped due to memory limit of {limit} bytes exceeded ({memory} bytes used)', ['limit' => $this->memoryLimit, 'memory' => $usedMemory]); + } + } + } + + public static function getSubscribedEvents() + { + return [ + WorkerRunningEvent::class => 'onWorkerRunning', + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnMessageLimitListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnMessageLimitListener.php new file mode 100644 index 0000000..ca71ff1 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnMessageLimitListener.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; + +/** + * @author Samuel Roze + * @author Tobias Schultze + */ +class StopWorkerOnMessageLimitListener implements EventSubscriberInterface +{ + private $maximumNumberOfMessages; + private $logger; + private $receivedMessages = 0; + + public function __construct(int $maximumNumberOfMessages, LoggerInterface $logger = null) + { + $this->maximumNumberOfMessages = $maximumNumberOfMessages; + $this->logger = $logger; + + if ($maximumNumberOfMessages <= 0) { + throw new InvalidArgumentException('Message limit must be greater than zero.'); + } + } + + public function onWorkerRunning(WorkerRunningEvent $event): void + { + if (!$event->isWorkerIdle() && ++$this->receivedMessages >= $this->maximumNumberOfMessages) { + $this->receivedMessages = 0; + $event->getWorker()->stop(); + + if (null !== $this->logger) { + $this->logger->info('Worker stopped due to maximum count of {count} messages processed', ['count' => $this->maximumNumberOfMessages]); + } + } + } + + public static function getSubscribedEvents() + { + return [ + WorkerRunningEvent::class => 'onWorkerRunning', + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnRestartSignalListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnRestartSignalListener.php new file mode 100644 index 0000000..0fb3d40 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnRestartSignalListener.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Event\WorkerStartedEvent; + +/** + * @author Ryan Weaver + */ +class StopWorkerOnRestartSignalListener implements EventSubscriberInterface +{ + public const RESTART_REQUESTED_TIMESTAMP_KEY = 'workers.restart_requested_timestamp'; + + private $cachePool; + private $logger; + private $workerStartedAt; + + public function __construct(CacheItemPoolInterface $cachePool, LoggerInterface $logger = null) + { + $this->cachePool = $cachePool; + $this->logger = $logger; + } + + public function onWorkerStarted(): void + { + $this->workerStartedAt = microtime(true); + } + + public function onWorkerRunning(WorkerRunningEvent $event): void + { + if ($this->shouldRestart()) { + $event->getWorker()->stop(); + if (null !== $this->logger) { + $this->logger->info('Worker stopped because a restart was requested.'); + } + } + } + + public static function getSubscribedEvents() + { + return [ + WorkerStartedEvent::class => 'onWorkerStarted', + WorkerRunningEvent::class => 'onWorkerRunning', + ]; + } + + private function shouldRestart(): bool + { + $cacheItem = $this->cachePool->getItem(self::RESTART_REQUESTED_TIMESTAMP_KEY); + + if (!$cacheItem->isHit()) { + // no restart has ever been scheduled + return false; + } + + return $this->workerStartedAt < $cacheItem->get(); + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnSigtermSignalListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnSigtermSignalListener.php new file mode 100644 index 0000000..c865546 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnSigtermSignalListener.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerStartedEvent; + +/** + * @author Tobias Schultze + */ +class StopWorkerOnSigtermSignalListener implements EventSubscriberInterface +{ + private $logger; + + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + public function onWorkerStarted(WorkerStartedEvent $event): void + { + pcntl_signal(\SIGTERM, function () use ($event) { + if (null !== $this->logger) { + $this->logger->info('Received SIGTERM signal.', ['transport_names' => $event->getWorker()->getMetadata()->getTransportNames()]); + } + + $event->getWorker()->stop(); + }); + } + + public static function getSubscribedEvents() + { + if (!\function_exists('pcntl_signal')) { + return []; + } + + return [ + WorkerStartedEvent::class => ['onWorkerStarted', 100], + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnTimeLimitListener.php b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnTimeLimitListener.php new file mode 100644 index 0000000..a3f982d --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/EventListener/StopWorkerOnTimeLimitListener.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Event\WorkerStartedEvent; + +/** + * @author Simon Delicata + * @author Tobias Schultze + */ +class StopWorkerOnTimeLimitListener implements EventSubscriberInterface +{ + private $timeLimitInSeconds; + private $logger; + private $endTime; + + public function __construct(int $timeLimitInSeconds, LoggerInterface $logger = null) + { + $this->timeLimitInSeconds = $timeLimitInSeconds; + $this->logger = $logger; + } + + public function onWorkerStarted(): void + { + $startTime = microtime(true); + $this->endTime = $startTime + $this->timeLimitInSeconds; + } + + public function onWorkerRunning(WorkerRunningEvent $event): void + { + if ($this->endTime < microtime(true)) { + $event->getWorker()->stop(); + if (null !== $this->logger) { + $this->logger->info('Worker stopped due to time limit of {timeLimit}s exceeded', ['timeLimit' => $this->timeLimitInSeconds]); + } + } + } + + public static function getSubscribedEvents() + { + return [ + WorkerStartedEvent::class => 'onWorkerStarted', + WorkerRunningEvent::class => 'onWorkerRunning', + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/DelayedMessageHandlingException.php b/plugins/email/vendor/symfony/messenger/Exception/DelayedMessageHandlingException.php new file mode 100644 index 0000000..4314a4f --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/DelayedMessageHandlingException.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * When handling queued messages from {@link DispatchAfterCurrentBusMiddleware}, + * some handlers caused an exception. This exception contains all those handler exceptions. + * + * @author Tobias Nyholm + */ +class DelayedMessageHandlingException extends RuntimeException +{ + private $exceptions; + + public function __construct(array $exceptions) + { + $exceptionMessages = implode(", \n", array_map( + function (\Throwable $e) { + return \get_class($e).': '.$e->getMessage(); + }, + $exceptions + )); + + if (1 === \count($exceptions)) { + $message = sprintf("A delayed message handler threw an exception: \n\n%s", $exceptionMessages); + } else { + $message = sprintf("Some delayed message handlers threw an exception: \n\n%s", $exceptionMessages); + } + + $this->exceptions = $exceptions; + + parent::__construct($message, 0, $exceptions[0]); + } + + public function getExceptions(): array + { + return $this->exceptions; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/ExceptionInterface.php b/plugins/email/vendor/symfony/messenger/Exception/ExceptionInterface.php new file mode 100644 index 0000000..02a72de --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * Base Messenger component's exception. + * + * @author Samuel Roze + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/HandlerFailedException.php b/plugins/email/vendor/symfony/messenger/Exception/HandlerFailedException.php new file mode 100644 index 0000000..6ac3169 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/HandlerFailedException.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +use Symfony\Component\Messenger\Envelope; + +class HandlerFailedException extends RuntimeException +{ + private $exceptions; + private $envelope; + + /** + * @param \Throwable[] $exceptions + */ + public function __construct(Envelope $envelope, array $exceptions) + { + $firstFailure = current($exceptions); + + $message = sprintf('Handling "%s" failed: ', \get_class($envelope->getMessage())); + + parent::__construct( + $message.(1 === \count($exceptions) + ? $firstFailure->getMessage() + : sprintf('%d handlers failed. First failure is: %s', \count($exceptions), $firstFailure->getMessage()) + ), + (int) $firstFailure->getCode(), + $firstFailure + ); + + $this->envelope = $envelope; + $this->exceptions = $exceptions; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + /** + * @return \Throwable[] + */ + public function getNestedExceptions(): array + { + return $this->exceptions; + } + + public function getNestedExceptionOfClass(string $exceptionClassName): array + { + return array_values( + array_filter( + $this->exceptions, + function ($exception) use ($exceptionClassName) { + return is_a($exception, $exceptionClassName); + } + ) + ); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/InvalidArgumentException.php b/plugins/email/vendor/symfony/messenger/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..a75c722 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Yonel Ceruto + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/LogicException.php b/plugins/email/vendor/symfony/messenger/Exception/LogicException.php new file mode 100644 index 0000000..75f97d2 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Roland Franssen + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/MessageDecodingFailedException.php b/plugins/email/vendor/symfony/messenger/Exception/MessageDecodingFailedException.php new file mode 100644 index 0000000..bea4ac5 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/MessageDecodingFailedException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * Thrown when a message cannot be decoded in a serializer. + */ +class MessageDecodingFailedException extends InvalidArgumentException +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/NoHandlerForMessageException.php b/plugins/email/vendor/symfony/messenger/Exception/NoHandlerForMessageException.php new file mode 100644 index 0000000..d5efb45 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/NoHandlerForMessageException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Samuel Roze + */ +class NoHandlerForMessageException extends LogicException +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/RecoverableExceptionInterface.php b/plugins/email/vendor/symfony/messenger/Exception/RecoverableExceptionInterface.php new file mode 100644 index 0000000..b49ddbf --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/RecoverableExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * Marker interface for exceptions to indicate that handling a message should have worked. + * + * If something goes wrong while handling a message that's received from a transport + * and the message should be retried, a handler can throw such an exception. + * + * @author Jérémy Derussé + */ +interface RecoverableExceptionInterface extends \Throwable +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/RecoverableMessageHandlingException.php b/plugins/email/vendor/symfony/messenger/Exception/RecoverableMessageHandlingException.php new file mode 100644 index 0000000..6514573 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/RecoverableMessageHandlingException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * A concrete implementation of RecoverableExceptionInterface that can be used directly. + * + * @author Frederic Bouchery + */ +class RecoverableMessageHandlingException extends RuntimeException implements RecoverableExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/RejectRedeliveredMessageException.php b/plugins/email/vendor/symfony/messenger/Exception/RejectRedeliveredMessageException.php new file mode 100644 index 0000000..0befccf --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/RejectRedeliveredMessageException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Tobias Schultze + */ +class RejectRedeliveredMessageException extends RuntimeException +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/RuntimeException.php b/plugins/email/vendor/symfony/messenger/Exception/RuntimeException.php new file mode 100644 index 0000000..2d6c7b3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Fabien Potencier + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/StopWorkerException.php b/plugins/email/vendor/symfony/messenger/Exception/StopWorkerException.php new file mode 100644 index 0000000..e53bd32 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/StopWorkerException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Grégoire Pineau + */ +class StopWorkerException extends RuntimeException implements StopWorkerExceptionInterface +{ + public function __construct(string $message = 'Worker should stop.', \Throwable $previous = null) + { + parent::__construct($message, 0, $previous); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/StopWorkerExceptionInterface.php b/plugins/email/vendor/symfony/messenger/Exception/StopWorkerExceptionInterface.php new file mode 100644 index 0000000..16019e8 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/StopWorkerExceptionInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Grégoire Pineau + */ +interface StopWorkerExceptionInterface extends \Throwable +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/TransportException.php b/plugins/email/vendor/symfony/messenger/Exception/TransportException.php new file mode 100644 index 0000000..79952b8 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/TransportException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Eric Masoero + */ +class TransportException extends RuntimeException +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/UnrecoverableExceptionInterface.php b/plugins/email/vendor/symfony/messenger/Exception/UnrecoverableExceptionInterface.php new file mode 100644 index 0000000..ba5addf --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/UnrecoverableExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * Marker interface for exceptions to indicate that handling a message will continue to fail. + * + * If something goes wrong while handling a message that's received from a transport + * and the message should not be retried, a handler can throw such an exception. + * + * @author Tobias Schultze + */ +interface UnrecoverableExceptionInterface extends \Throwable +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/UnrecoverableMessageHandlingException.php b/plugins/email/vendor/symfony/messenger/Exception/UnrecoverableMessageHandlingException.php new file mode 100644 index 0000000..31616fb --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/UnrecoverableMessageHandlingException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * A concrete implementation of UnrecoverableExceptionInterface that can be used directly. + * + * @author Frederic Bouchery + */ +class UnrecoverableMessageHandlingException extends RuntimeException implements UnrecoverableExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Exception/ValidationFailedException.php b/plugins/email/vendor/symfony/messenger/Exception/ValidationFailedException.php new file mode 100644 index 0000000..fe129cd --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Exception/ValidationFailedException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * @author Tobias Nyholm + */ +class ValidationFailedException extends RuntimeException +{ + private $violations; + private $violatingMessage; + + public function __construct(object $violatingMessage, ConstraintViolationListInterface $violations) + { + $this->violatingMessage = $violatingMessage; + $this->violations = $violations; + + parent::__construct(sprintf('Message of type "%s" failed validation.', \get_class($this->violatingMessage))); + } + + public function getViolatingMessage() + { + return $this->violatingMessage; + } + + public function getViolations(): ConstraintViolationListInterface + { + return $this->violations; + } +} diff --git a/plugins/email/vendor/symfony/messenger/HandleTrait.php b/plugins/email/vendor/symfony/messenger/HandleTrait.php new file mode 100644 index 0000000..4f77a93 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/HandleTrait.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Stamp\HandledStamp; + +/** + * Leverages a message bus to expect a single, synchronous message handling and return its result. + * + * @author Maxime Steinhausser + */ +trait HandleTrait +{ + /** @var MessageBusInterface */ + private $messageBus; + + /** + * Dispatches the given message, expecting to be handled by a single handler + * and returns the result from the handler returned value. + * This behavior is useful for both synchronous command & query buses, + * the last one usually returning the handler result. + * + * @param object|Envelope $message The message or the message pre-wrapped in an envelope + * + * @return mixed + */ + private function handle(object $message) + { + if (!$this->messageBus instanceof MessageBusInterface) { + throw new LogicException(sprintf('You must provide a "%s" instance in the "%s::$messageBus" property, "%s" given.', MessageBusInterface::class, static::class, get_debug_type($this->messageBus))); + } + + $envelope = $this->messageBus->dispatch($message); + /** @var HandledStamp[] $handledStamps */ + $handledStamps = $envelope->all(HandledStamp::class); + + if (!$handledStamps) { + throw new LogicException(sprintf('Message of type "%s" was handled zero times. Exactly one handler is expected when using "%s::%s()".', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__)); + } + + if (\count($handledStamps) > 1) { + $handlers = implode(', ', array_map(function (HandledStamp $stamp): string { + return sprintf('"%s"', $stamp->getHandlerName()); + }, $handledStamps)); + + throw new LogicException(sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: %s.', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__, \count($handledStamps), $handlers)); + } + + return $handledStamps[0]->getResult(); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/Acknowledger.php b/plugins/email/vendor/symfony/messenger/Handler/Acknowledger.php new file mode 100644 index 0000000..a2317b7 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/Acknowledger.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +use Symfony\Component\Messenger\Exception\LogicException; + +/** + * @author Nicolas Grekas + */ +class Acknowledger +{ + private $handlerClass; + private $ack; + private $error = null; + private $result = null; + + /** + * @param null|\Closure(\Throwable|null, mixed):void $ack + */ + public function __construct(string $handlerClass, \Closure $ack = null) + { + $this->handlerClass = $handlerClass; + $this->ack = $ack ?? static function () {}; + } + + /** + * @param mixed $result + */ + public function ack($result = null): void + { + $this->doAck(null, $result); + } + + public function nack(\Throwable $error): void + { + $this->doAck($error); + } + + public function getError(): ?\Throwable + { + return $this->error; + } + + /** + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + public function isAcknowledged(): bool + { + return null === $this->ack; + } + + public function __destruct() + { + if ($this->ack instanceof \Closure) { + throw new LogicException(sprintf('The acknowledger was not called by the "%s" batch handler.', $this->handlerClass)); + } + } + + private function doAck(\Throwable $e = null, $result = null): void + { + if (!$ack = $this->ack) { + throw new LogicException(sprintf('The acknowledger cannot be called twice by the "%s" batch handler.', $this->handlerClass)); + } + $this->ack = null; + $this->error = $e; + $this->result = $result; + $ack($e, $result); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/BatchHandlerInterface.php b/plugins/email/vendor/symfony/messenger/Handler/BatchHandlerInterface.php new file mode 100644 index 0000000..a2fce4e --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/BatchHandlerInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +/** + * @author Nicolas Grekas + */ +interface BatchHandlerInterface +{ + /** + * @param Acknowledger|null $ack The function to call to ack/nack the $message. + * The message should be handled synchronously when null. + * + * @return mixed The number of pending messages in the batch if $ack is not null, + * the result from handling the message otherwise + */ + // public function __invoke(object $message, Acknowledger $ack = null): mixed; + + /** + * Flushes any pending buffers. + * + * @param bool $force Whether flushing is required; it can be skipped if not + */ + public function flush(bool $force): void; +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/BatchHandlerTrait.php b/plugins/email/vendor/symfony/messenger/Handler/BatchHandlerTrait.php new file mode 100644 index 0000000..be7124d --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/BatchHandlerTrait.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +use Symfony\Component\Messenger\Exception\LogicException; + +/** + * @author Nicolas Grekas + */ +trait BatchHandlerTrait +{ + private $jobs = []; + + /** + * {@inheritdoc} + */ + public function flush(bool $force): void + { + if ($jobs = $this->jobs) { + $this->jobs = []; + $this->process($jobs); + } + } + + /** + * @param Acknowledger|null $ack The function to call to ack/nack the $message. + * The message should be handled synchronously when null. + * + * @return mixed The number of pending messages in the batch if $ack is not null, + * the result from handling the message otherwise + */ + private function handle(object $message, ?Acknowledger $ack) + { + if (null === $ack) { + $ack = new Acknowledger(get_debug_type($this)); + $this->jobs[] = [$message, $ack]; + $this->flush(true); + + return $ack->getResult(); + } + + $this->jobs[] = [$message, $ack]; + if (!$this->shouldFlush()) { + return \count($this->jobs); + } + + $this->flush(true); + + return 0; + } + + private function shouldFlush(): bool + { + return 10 <= \count($this->jobs); + } + + /** + * Completes the jobs in the list. + * + * @list $jobs A list of pairs of messages and their corresponding acknowledgers + */ + private function process(array $jobs): void + { + throw new LogicException(sprintf('"%s" should implement abstract method "process()".', get_debug_type($this))); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/HandlerDescriptor.php b/plugins/email/vendor/symfony/messenger/Handler/HandlerDescriptor.php new file mode 100644 index 0000000..6acb2c2 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/HandlerDescriptor.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +/** + * Describes a handler and the possible associated options, such as `from_transport`, `bus`, etc. + * + * @author Samuel Roze + */ +final class HandlerDescriptor +{ + private $handler; + private $name; + private $batchHandler; + private $options; + + public function __construct(callable $handler, array $options = []) + { + if (!$handler instanceof \Closure) { + $handler = \Closure::fromCallable($handler); + } + + $this->handler = $handler; + $this->options = $options; + + $r = new \ReflectionFunction($handler); + + if (str_contains($r->name, '{closure}')) { + $this->name = 'Closure'; + } elseif (!$handler = $r->getClosureThis()) { + $class = $r->getClosureScopeClass(); + + $this->name = ($class ? $class->name.'::' : '').$r->name; + } else { + if ($handler instanceof BatchHandlerInterface) { + $this->batchHandler = $handler; + } + + $this->name = \get_class($handler).'::'.$r->name; + } + } + + public function getHandler(): callable + { + return $this->handler; + } + + public function getName(): string + { + $name = $this->name; + $alias = $this->options['alias'] ?? null; + + if (null !== $alias) { + $name .= '@'.$alias; + } + + return $name; + } + + public function getBatchHandler(): ?BatchHandlerInterface + { + return $this->batchHandler; + } + + public function getOption(string $option) + { + return $this->options[$option] ?? null; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/HandlersLocator.php b/plugins/email/vendor/symfony/messenger/Handler/HandlersLocator.php new file mode 100644 index 0000000..6252bcc --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/HandlersLocator.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\ReceivedStamp; + +/** + * Maps a message to a list of handlers. + * + * @author Nicolas Grekas + * @author Samuel Roze + */ +class HandlersLocator implements HandlersLocatorInterface +{ + private $handlers; + + /** + * @param HandlerDescriptor[][]|callable[][] $handlers + */ + public function __construct(array $handlers) + { + $this->handlers = $handlers; + } + + /** + * {@inheritdoc} + */ + public function getHandlers(Envelope $envelope): iterable + { + $seen = []; + + foreach (self::listTypes($envelope) as $type) { + foreach ($this->handlers[$type] ?? [] as $handlerDescriptor) { + if (\is_callable($handlerDescriptor)) { + $handlerDescriptor = new HandlerDescriptor($handlerDescriptor); + } + + if (!$this->shouldHandle($envelope, $handlerDescriptor)) { + continue; + } + + $name = $handlerDescriptor->getName(); + if (\in_array($name, $seen)) { + continue; + } + + $seen[] = $name; + + yield $handlerDescriptor; + } + } + } + + /** + * @internal + */ + public static function listTypes(Envelope $envelope): array + { + $class = \get_class($envelope->getMessage()); + + return [$class => $class] + + class_parents($class) + + class_implements($class) + + ['*' => '*']; + } + + private function shouldHandle(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool + { + if (null === $received = $envelope->last(ReceivedStamp::class)) { + return true; + } + + if (null === $expectedTransport = $handlerDescriptor->getOption('from_transport')) { + return true; + } + + return $received->getTransportName() === $expectedTransport; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/HandlersLocatorInterface.php b/plugins/email/vendor/symfony/messenger/Handler/HandlersLocatorInterface.php new file mode 100644 index 0000000..c0ac07f --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/HandlersLocatorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +use Symfony\Component\Messenger\Envelope; + +/** + * Maps a message to a list of handlers. + * + * @author Nicolas Grekas + */ +interface HandlersLocatorInterface +{ + /** + * Returns the handlers for the given message name. + * + * @return iterable + */ + public function getHandlers(Envelope $envelope): iterable; +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/MessageHandlerInterface.php b/plugins/email/vendor/symfony/messenger/Handler/MessageHandlerInterface.php new file mode 100644 index 0000000..7b219a3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/MessageHandlerInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +/** + * Marker interface for message handlers. + * + * @author Samuel Roze + */ +interface MessageHandlerInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Handler/MessageSubscriberInterface.php b/plugins/email/vendor/symfony/messenger/Handler/MessageSubscriberInterface.php new file mode 100644 index 0000000..2eca1a9 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Handler/MessageSubscriberInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +/** + * Handlers can implement this interface to handle multiple messages. + * + * @author Samuel Roze + */ +interface MessageSubscriberInterface extends MessageHandlerInterface +{ + /** + * Returns a list of messages to be handled. + * + * It returns a list of messages like in the following example: + * + * yield MyMessage::class; + * + * It can also change the priority per classes. + * + * yield FirstMessage::class => ['priority' => 0]; + * yield SecondMessage::class => ['priority' => -10]; + * + * It can also specify a method, a priority, a bus and/or a transport per message: + * + * yield FirstMessage::class => ['method' => 'firstMessageMethod']; + * yield SecondMessage::class => [ + * 'method' => 'secondMessageMethod', + * 'priority' => 20, + * 'bus' => 'my_bus_name', + * 'from_transport' => 'your_transport_name', + * ]; + * + * The benefit of using `yield` instead of returning an array is that you can `yield` multiple times the + * same key and therefore subscribe to the same message multiple times with different options. + * + * The `__invoke` method of the handler will be called as usual with the message to handle. + */ + public static function getHandledMessages(): iterable; +} diff --git a/plugins/email/vendor/symfony/messenger/LICENSE b/plugins/email/vendor/symfony/messenger/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/messenger/MessageBus.php b/plugins/email/vendor/symfony/messenger/MessageBus.php new file mode 100644 index 0000000..3db9fab --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/MessageBus.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Middleware\StackMiddleware; + +/** + * @author Samuel Roze + * @author Matthias Noback + * @author Nicolas Grekas + */ +class MessageBus implements MessageBusInterface +{ + private $middlewareAggregate; + + /** + * @param iterable $middlewareHandlers + */ + public function __construct(iterable $middlewareHandlers = []) + { + if ($middlewareHandlers instanceof \IteratorAggregate) { + $this->middlewareAggregate = $middlewareHandlers; + } elseif (\is_array($middlewareHandlers)) { + $this->middlewareAggregate = new \ArrayObject($middlewareHandlers); + } else { + // $this->middlewareAggregate should be an instance of IteratorAggregate. + // When $middlewareHandlers is an Iterator, we wrap it to ensure it is lazy-loaded and can be rewound. + $this->middlewareAggregate = new class($middlewareHandlers) implements \IteratorAggregate { + private $middlewareHandlers; + private $cachedIterator; + + public function __construct(\Traversable $middlewareHandlers) + { + $this->middlewareHandlers = $middlewareHandlers; + } + + public function getIterator(): \Traversable + { + if (null === $this->cachedIterator) { + $this->cachedIterator = new \ArrayObject(iterator_to_array($this->middlewareHandlers, false)); + } + + return $this->cachedIterator; + } + }; + } + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $message, array $stamps = []): Envelope + { + $envelope = Envelope::wrap($message, $stamps); + $middlewareIterator = $this->middlewareAggregate->getIterator(); + + while ($middlewareIterator instanceof \IteratorAggregate) { + $middlewareIterator = $middlewareIterator->getIterator(); + } + $middlewareIterator->rewind(); + + if (!$middlewareIterator->valid()) { + return $envelope; + } + $stack = new StackMiddleware($middlewareIterator); + + return $middlewareIterator->current()->handle($envelope, $stack); + } +} diff --git a/plugins/email/vendor/symfony/messenger/MessageBusInterface.php b/plugins/email/vendor/symfony/messenger/MessageBusInterface.php new file mode 100644 index 0000000..f1dbe0a --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/MessageBusInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Symfony\Component\Messenger\Stamp\StampInterface; + +/** + * @author Samuel Roze + */ +interface MessageBusInterface +{ + /** + * Dispatches the given message. + * + * @param object|Envelope $message The message or the message pre-wrapped in an envelope + * @param StampInterface[] $stamps + */ + public function dispatch(object $message, array $stamps = []): Envelope; +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/ActivationMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/ActivationMiddleware.php new file mode 100644 index 0000000..8d101e4 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/ActivationMiddleware.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; + +/** + * Execute the inner middleware according to an activation strategy. + * + * @author Maxime Steinhausser + */ +class ActivationMiddleware implements MiddlewareInterface +{ + private $inner; + private $activated; + + /** + * @param bool|callable $activated + */ + public function __construct(MiddlewareInterface $inner, $activated) + { + $this->inner = $inner; + $this->activated = $activated; + } + + /** + * {@inheritdoc} + */ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + if (\is_callable($this->activated) ? ($this->activated)($envelope) : $this->activated) { + return $this->inner->handle($envelope, $stack); + } + + return $stack->next()->handle($envelope, $stack); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/AddBusNameStampMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/AddBusNameStampMiddleware.php new file mode 100644 index 0000000..925fd5f --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/AddBusNameStampMiddleware.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\BusNameStamp; + +/** + * Adds the BusNameStamp to the bus. + * + * @author Ryan Weaver + */ +class AddBusNameStampMiddleware implements MiddlewareInterface +{ + private $busName; + + public function __construct(string $busName) + { + $this->busName = $busName; + } + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + if (null === $envelope->last(BusNameStamp::class)) { + $envelope = $envelope->with(new BusNameStamp($this->busName)); + } + + return $stack->next()->handle($envelope, $stack); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/DispatchAfterCurrentBusMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/DispatchAfterCurrentBusMiddleware.php new file mode 100644 index 0000000..a088140 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/DispatchAfterCurrentBusMiddleware.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException; +use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; + +/** + * Allow to configure messages to be handled after the current bus is finished. + * + * I.e, messages dispatched from a handler with a DispatchAfterCurrentBus stamp + * will actually be handled once the current message being dispatched is fully + * handled. + * + * For instance, using this middleware before the DoctrineTransactionMiddleware + * means sub-dispatched messages with a DispatchAfterCurrentBus stamp would be + * handled after the Doctrine transaction has been committed. + * + * @author Tobias Nyholm + */ +class DispatchAfterCurrentBusMiddleware implements MiddlewareInterface +{ + /** + * @var QueuedEnvelope[] A queue of messages and next middleware + */ + private $queue = []; + + /** + * @var bool this property is used to signal if we are inside a the first/root call to + * MessageBusInterface::dispatch() or if dispatch has been called inside a message handler + */ + private $isRootDispatchCallRunning = false; + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + if (null !== $envelope->last(DispatchAfterCurrentBusStamp::class)) { + if ($this->isRootDispatchCallRunning) { + $this->queue[] = new QueuedEnvelope($envelope, $stack); + + return $envelope; + } + + $envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class); + } + + if ($this->isRootDispatchCallRunning) { + /* + * A call to MessageBusInterface::dispatch() was made from inside the main bus handling, + * but the message does not have the stamp. So, process it like normal. + */ + return $stack->next()->handle($envelope, $stack); + } + + // First time we get here, mark as inside a "root dispatch" call: + $this->isRootDispatchCallRunning = true; + try { + // Execute the whole middleware stack & message handling for main dispatch: + $returnedEnvelope = $stack->next()->handle($envelope, $stack); + } catch (\Throwable $exception) { + /* + * Whenever an exception occurs while handling a message that has + * queued other messages, we drop the queued ones. + * This is intentional since the queued commands were likely dependent + * on the preceding command. + */ + $this->queue = []; + $this->isRootDispatchCallRunning = false; + + throw $exception; + } + + // "Root dispatch" call is finished, dispatch stored messages. + $exceptions = []; + while (null !== $queueItem = array_shift($this->queue)) { + // Save how many messages are left in queue before handling the message + $queueLengthBefore = \count($this->queue); + try { + // Execute the stored messages + $queueItem->getStack()->next()->handle($queueItem->getEnvelope(), $queueItem->getStack()); + } catch (\Exception $exception) { + // Gather all exceptions + $exceptions[] = $exception; + // Restore queue to previous state + $this->queue = \array_slice($this->queue, 0, $queueLengthBefore); + } + } + + $this->isRootDispatchCallRunning = false; + if (\count($exceptions) > 0) { + throw new DelayedMessageHandlingException($exceptions); + } + + return $returnedEnvelope; + } +} + +/** + * @internal + */ +final class QueuedEnvelope +{ + /** @var Envelope */ + private $envelope; + + /** @var StackInterface */ + private $stack; + + public function __construct(Envelope $envelope, StackInterface $stack) + { + $this->envelope = $envelope->withoutAll(DispatchAfterCurrentBusStamp::class); + $this->stack = $stack; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + public function getStack(): StackInterface + { + return $this->stack; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/FailedMessageProcessingMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/FailedMessageProcessingMiddleware.php new file mode 100644 index 0000000..6e40d11 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/FailedMessageProcessingMiddleware.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\ReceivedStamp; +use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; + +/** + * @author Ryan Weaver + */ +class FailedMessageProcessingMiddleware implements MiddlewareInterface +{ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + // look for "received" messages decorated with the SentToFailureTransportStamp + /** @var SentToFailureTransportStamp|null $sentToFailureStamp */ + $sentToFailureStamp = $envelope->last(SentToFailureTransportStamp::class); + if (null !== $sentToFailureStamp && null !== $envelope->last(ReceivedStamp::class)) { + // mark the message as "received" from the original transport + // this guarantees the same behavior as when originally received + $envelope = $envelope->with(new ReceivedStamp($sentToFailureStamp->getOriginalReceiverName())); + } + + return $stack->next()->handle($envelope, $stack); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php new file mode 100644 index 0000000..149f304 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Exception\NoHandlerForMessageException; +use Symfony\Component\Messenger\Handler\Acknowledger; +use Symfony\Component\Messenger\Handler\HandlerDescriptor; +use Symfony\Component\Messenger\Handler\HandlersLocatorInterface; +use Symfony\Component\Messenger\Stamp\AckStamp; +use Symfony\Component\Messenger\Stamp\FlushBatchHandlersStamp; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Symfony\Component\Messenger\Stamp\NoAutoAckStamp; + +/** + * @author Samuel Roze + */ +class HandleMessageMiddleware implements MiddlewareInterface +{ + use LoggerAwareTrait; + + private $handlersLocator; + private $allowNoHandlers; + + public function __construct(HandlersLocatorInterface $handlersLocator, bool $allowNoHandlers = false) + { + $this->handlersLocator = $handlersLocator; + $this->allowNoHandlers = $allowNoHandlers; + $this->logger = new NullLogger(); + } + + /** + * {@inheritdoc} + * + * @throws NoHandlerForMessageException When no handler is found and $allowNoHandlers is false + */ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $handler = null; + $message = $envelope->getMessage(); + + $context = [ + 'class' => \get_class($message), + ]; + + $exceptions = []; + foreach ($this->handlersLocator->getHandlers($envelope) as $handlerDescriptor) { + if ($this->messageHasAlreadyBeenHandled($envelope, $handlerDescriptor)) { + continue; + } + + try { + $handler = $handlerDescriptor->getHandler(); + $batchHandler = $handlerDescriptor->getBatchHandler(); + + /** @var AckStamp $ackStamp */ + if ($batchHandler && $ackStamp = $envelope->last(AckStamp::class)) { + $ack = new Acknowledger(get_debug_type($batchHandler), static function (\Throwable $e = null, $result = null) use ($envelope, $ackStamp, $handlerDescriptor) { + if (null !== $e) { + $e = new HandlerFailedException($envelope, [$e]); + } else { + $envelope = $envelope->with(HandledStamp::fromDescriptor($handlerDescriptor, $result)); + } + + $ackStamp->ack($envelope, $e); + }); + + $result = $handler($message, $ack); + + if (!\is_int($result) || 0 > $result) { + throw new LogicException(sprintf('A handler implementing BatchHandlerInterface must return the size of the current batch as a positive integer, "%s" returned from "%s".', \is_int($result) ? $result : get_debug_type($result), get_debug_type($batchHandler))); + } + + if (!$ack->isAcknowledged()) { + $envelope = $envelope->with(new NoAutoAckStamp($handlerDescriptor)); + } elseif ($ack->getError()) { + throw $ack->getError(); + } else { + $result = $ack->getResult(); + } + } else { + $result = $handler($message); + } + + $handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $result); + $envelope = $envelope->with($handledStamp); + $this->logger->info('Message {class} handled by {handler}', $context + ['handler' => $handledStamp->getHandlerName()]); + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + + /** @var FlushBatchHandlersStamp $flushStamp */ + if ($flushStamp = $envelope->last(FlushBatchHandlersStamp::class)) { + /** @var NoAutoAckStamp $stamp */ + foreach ($envelope->all(NoAutoAckStamp::class) as $stamp) { + try { + $handler = $stamp->getHandlerDescriptor()->getBatchHandler(); + $handler->flush($flushStamp->force()); + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + } + + if (null === $handler) { + if (!$this->allowNoHandlers) { + throw new NoHandlerForMessageException(sprintf('No handler for message "%s".', $context['class'])); + } + + $this->logger->info('No handler for message {class}', $context); + } + + if (\count($exceptions)) { + throw new HandlerFailedException($envelope, $exceptions); + } + + return $stack->next()->handle($envelope, $stack); + } + + private function messageHasAlreadyBeenHandled(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool + { + /** @var HandledStamp $stamp */ + foreach ($envelope->all(HandledStamp::class) as $stamp) { + if ($stamp->getHandlerName() === $handlerDescriptor->getName()) { + return true; + } + } + + return false; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/MiddlewareInterface.php b/plugins/email/vendor/symfony/messenger/Middleware/MiddlewareInterface.php new file mode 100644 index 0000000..9826611 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/MiddlewareInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; + +/** + * @author Samuel Roze + */ +interface MiddlewareInterface +{ + public function handle(Envelope $envelope, StackInterface $stack): Envelope; +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/RejectRedeliveredMessageMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/RejectRedeliveredMessageMiddleware.php new file mode 100644 index 0000000..9e994dd --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/RejectRedeliveredMessageMiddleware.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp as LegacyAmqpReceivedStamp; + +/** + * Middleware that throws a RejectRedeliveredMessageException when a message is detected that has been redelivered by AMQP. + * + * The middleware runs before the HandleMessageMiddleware and prevents redelivered messages from being handled directly. + * The thrown exception is caught by the worker and will trigger the retry logic according to the retry strategy. + * + * AMQP redelivers messages when they do not get acknowledged or rejected. This can happen when the connection times out + * or an exception is thrown before acknowledging or rejecting. When such errors happen again while handling the + * redelivered message, the message would get redelivered again and again. The purpose of this middleware is to prevent + * infinite redelivery loops and to unblock the queue by republishing the redelivered messages as retries with a retry + * limit and potential delay. + * + * @author Tobias Schultze + */ +class RejectRedeliveredMessageMiddleware implements MiddlewareInterface +{ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $amqpReceivedStamp = $envelope->last(AmqpReceivedStamp::class); + if ($amqpReceivedStamp instanceof AmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) { + throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.'); + } + + // Legacy code to support symfony/messenger < 5.1 + $amqpReceivedStamp = $envelope->last(LegacyAmqpReceivedStamp::class); + if ($amqpReceivedStamp instanceof LegacyAmqpReceivedStamp && $amqpReceivedStamp->getAmqpEnvelope()->isRedelivery()) { + throw new RejectRedeliveredMessageException('Redelivered message from AMQP detected that will be rejected and trigger the retry logic.'); + } + + return $stack->next()->handle($envelope, $stack); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/RouterContextMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/RouterContextMiddleware.php new file mode 100644 index 0000000..62bd1d7 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/RouterContextMiddleware.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; +use Symfony\Component\Messenger\Stamp\RouterContextStamp; +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * Restore the Router context when processing the message. + * + * @author Jérémy Derussé + */ +class RouterContextMiddleware implements MiddlewareInterface +{ + private $router; + + public function __construct(RequestContextAwareInterface $router) + { + $this->router = $router; + } + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + if (!$envelope->last(ConsumedByWorkerStamp::class) || !$contextStamp = $envelope->last(RouterContextStamp::class)) { + $context = $this->router->getContext(); + $envelope = $envelope->with(new RouterContextStamp( + $context->getBaseUrl(), + $context->getMethod(), + $context->getHost(), + $context->getScheme(), + $context->getHttpPort(), + $context->getHttpsPort(), + $context->getPathInfo(), + $context->getQueryString() + )); + + return $stack->next()->handle($envelope, $stack); + } + + $context = $this->router->getContext(); + $currentBaseUrl = $context->getBaseUrl(); + $currentMethod = $context->getMethod(); + $currentHost = $context->getHost(); + $currentScheme = $context->getScheme(); + $currentHttpPort = $context->getHttpPort(); + $currentHttpsPort = $context->getHttpsPort(); + $currentPathInfo = $context->getPathInfo(); + $currentQueryString = $context->getQueryString(); + + /* @var RouterContextStamp $contextStamp */ + $context + ->setBaseUrl($contextStamp->getBaseUrl()) + ->setMethod($contextStamp->getMethod()) + ->setHost($contextStamp->getHost()) + ->setScheme($contextStamp->getScheme()) + ->setHttpPort($contextStamp->getHttpPort()) + ->setHttpsPort($contextStamp->getHttpsPort()) + ->setPathInfo($contextStamp->getPathInfo()) + ->setQueryString($contextStamp->getQueryString()) + ; + + try { + return $stack->next()->handle($envelope, $stack); + } finally { + $context + ->setBaseUrl($currentBaseUrl) + ->setMethod($currentMethod) + ->setHost($currentHost) + ->setScheme($currentScheme) + ->setHttpPort($currentHttpPort) + ->setHttpsPort($currentHttpsPort) + ->setPathInfo($currentPathInfo) + ->setQueryString($currentQueryString) + ; + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/SendMessageMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/SendMessageMiddleware.php new file mode 100644 index 0000000..669fe76 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/SendMessageMiddleware.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; +use Symfony\Component\Messenger\Stamp\ReceivedStamp; +use Symfony\Component\Messenger\Stamp\SentStamp; +use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * @author Samuel Roze + * @author Tobias Schultze + */ +class SendMessageMiddleware implements MiddlewareInterface +{ + use LoggerAwareTrait; + + private $sendersLocator; + private $eventDispatcher; + + public function __construct(SendersLocatorInterface $sendersLocator, EventDispatcherInterface $eventDispatcher = null) + { + $this->sendersLocator = $sendersLocator; + $this->eventDispatcher = class_exists(Event::class) ? LegacyEventDispatcherProxy::decorate($eventDispatcher) : $eventDispatcher; + $this->logger = new NullLogger(); + } + + /** + * {@inheritdoc} + */ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $context = [ + 'class' => \get_class($envelope->getMessage()), + ]; + + $sender = null; + + if ($envelope->all(ReceivedStamp::class)) { + // it's a received message, do not send it back + $this->logger->info('Received message {class}', $context); + } else { + $shouldDispatchEvent = true; + foreach ($this->sendersLocator->getSenders($envelope) as $alias => $sender) { + if (null !== $this->eventDispatcher && $shouldDispatchEvent) { + $event = new SendMessageToTransportsEvent($envelope); + $this->eventDispatcher->dispatch($event); + $envelope = $event->getEnvelope(); + $shouldDispatchEvent = false; + } + + $this->logger->info('Sending message {class} with {alias} sender using {sender}', $context + ['alias' => $alias, 'sender' => \get_class($sender)]); + $envelope = $sender->send($envelope->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null))); + } + } + + if (null === $sender) { + return $stack->next()->handle($envelope, $stack); + } + + // message should only be sent and not be handled by the next middleware + return $envelope; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/StackInterface.php b/plugins/email/vendor/symfony/messenger/Middleware/StackInterface.php new file mode 100644 index 0000000..6e922c5 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/StackInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +/** + * @author Nicolas Grekas + * + * Implementations must be cloneable, and each clone must unstack the stack independently. + */ +interface StackInterface +{ + /** + * Returns the next middleware to process a message. + */ + public function next(): MiddlewareInterface; +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/StackMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/StackMiddleware.php new file mode 100644 index 0000000..30c181f --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/StackMiddleware.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; + +/** + * @author Nicolas Grekas + */ +class StackMiddleware implements MiddlewareInterface, StackInterface +{ + private $stack; + private $offset = 0; + + /** + * @param iterable|MiddlewareInterface|null $middlewareIterator + */ + public function __construct($middlewareIterator = null) + { + $this->stack = new MiddlewareStack(); + + if (null === $middlewareIterator) { + return; + } + + if ($middlewareIterator instanceof \Iterator) { + $this->stack->iterator = $middlewareIterator; + } elseif ($middlewareIterator instanceof MiddlewareInterface) { + $this->stack->stack[] = $middlewareIterator; + } elseif (!is_iterable($middlewareIterator)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be iterable of "%s", "%s" given.', __METHOD__, MiddlewareInterface::class, get_debug_type($middlewareIterator))); + } else { + $this->stack->iterator = (function () use ($middlewareIterator) { + yield from $middlewareIterator; + })(); + } + } + + public function next(): MiddlewareInterface + { + if (null === $next = $this->stack->next($this->offset)) { + return $this; + } + + ++$this->offset; + + return $next; + } + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + return $envelope; + } +} + +/** + * @internal + */ +class MiddlewareStack +{ + /** @var \Iterator */ + public $iterator; + public $stack = []; + + public function next(int $offset): ?MiddlewareInterface + { + if (isset($this->stack[$offset])) { + return $this->stack[$offset]; + } + + if (null === $this->iterator) { + return null; + } + + $this->iterator->next(); + + if (!$this->iterator->valid()) { + return $this->iterator = null; + } + + return $this->stack[] = $this->iterator->current(); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/TraceableMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/TraceableMiddleware.php new file mode 100644 index 0000000..f391cfe --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/TraceableMiddleware.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * Collects some data about a middleware. + * + * @author Maxime Steinhausser + */ +class TraceableMiddleware implements MiddlewareInterface +{ + private $stopwatch; + private $busName; + private $eventCategory; + + public function __construct(Stopwatch $stopwatch, string $busName, string $eventCategory = 'messenger.middleware') + { + $this->stopwatch = $stopwatch; + $this->busName = $busName; + $this->eventCategory = $eventCategory; + } + + /** + * {@inheritdoc} + */ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $stack = new TraceableStack($stack, $this->stopwatch, $this->busName, $this->eventCategory); + + try { + return $stack->next()->handle($envelope, $stack); + } finally { + $stack->stop(); + } + } +} + +/** + * @internal + */ +class TraceableStack implements StackInterface +{ + private $stack; + private $stopwatch; + private $busName; + private $eventCategory; + private $currentEvent; + + public function __construct(StackInterface $stack, Stopwatch $stopwatch, string $busName, string $eventCategory) + { + $this->stack = $stack; + $this->stopwatch = $stopwatch; + $this->busName = $busName; + $this->eventCategory = $eventCategory; + } + + /** + * {@inheritdoc} + */ + public function next(): MiddlewareInterface + { + if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) { + $this->stopwatch->stop($this->currentEvent); + } + + if ($this->stack === $nextMiddleware = $this->stack->next()) { + $this->currentEvent = 'Tail'; + } else { + $this->currentEvent = sprintf('"%s"', get_debug_type($nextMiddleware)); + } + $this->currentEvent .= sprintf(' on "%s"', $this->busName); + + $this->stopwatch->start($this->currentEvent, $this->eventCategory); + + return $nextMiddleware; + } + + public function stop(): void + { + if (null !== $this->currentEvent && $this->stopwatch->isStarted($this->currentEvent)) { + $this->stopwatch->stop($this->currentEvent); + } + $this->currentEvent = null; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Middleware/ValidationMiddleware.php b/plugins/email/vendor/symfony/messenger/Middleware/ValidationMiddleware.php new file mode 100644 index 0000000..fb199dd --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Middleware/ValidationMiddleware.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\ValidationFailedException; +use Symfony\Component\Messenger\Stamp\ValidationStamp; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @author Tobias Nyholm + */ +class ValidationMiddleware implements MiddlewareInterface +{ + private $validator; + + public function __construct(ValidatorInterface $validator) + { + $this->validator = $validator; + } + + /** + * {@inheritdoc} + */ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $message = $envelope->getMessage(); + $groups = null; + /** @var ValidationStamp|null $validationStamp */ + if ($validationStamp = $envelope->last(ValidationStamp::class)) { + $groups = $validationStamp->getGroups(); + } + + $violations = $this->validator->validate($message, null, $groups); + if (\count($violations)) { + throw new ValidationFailedException($message, $violations); + } + + return $stack->next()->handle($envelope, $stack); + } +} diff --git a/plugins/email/vendor/symfony/messenger/README.md b/plugins/email/vendor/symfony/messenger/README.md new file mode 100644 index 0000000..644269c --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/README.md @@ -0,0 +1,29 @@ +Messenger Component +=================== + +The Messenger component helps applications send and receive messages to/from +other applications or via message queues. + +Sponsor +------- + +The Messenger component for Symfony 5.4/6.0 is [backed][1] by [SensioLabs][2]. + +As the creator of Symfony, SensioLabs supports companies using Symfony, with an +offering encompassing consultancy, expertise, services, training, and technical +assistance to ensure the success of web application development projects. + +Help Symfony by [sponsoring][3] its development! + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/messenger.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) + +[1]: https://symfony.com/backers +[2]: https://sensiolabs.com +[3]: https://symfony.com/sponsor diff --git a/plugins/email/vendor/symfony/messenger/Retry/MultiplierRetryStrategy.php b/plugins/email/vendor/symfony/messenger/Retry/MultiplierRetryStrategy.php new file mode 100644 index 0000000..c081830 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Retry/MultiplierRetryStrategy.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Retry; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; + +/** + * A retry strategy with a constant or exponential retry delay. + * + * For example, if $delayMilliseconds=10000 & $multiplier=1 (default), + * each retry will wait exactly 10 seconds. + * + * But if $delayMilliseconds=10000 & $multiplier=2: + * * Retry 1: 10 second delay + * * Retry 2: 20 second delay (10000 * 2 = 20000) + * * Retry 3: 40 second delay (20000 * 2 = 40000) + * + * @author Ryan Weaver + * + * @final + */ +class MultiplierRetryStrategy implements RetryStrategyInterface +{ + private $maxRetries; + private $delayMilliseconds; + private $multiplier; + private $maxDelayMilliseconds; + + /** + * @param int $maxRetries The maximum number of times to retry + * @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used) + * @param float $multiplier Multiplier to apply to the delay each time a retry occurs + * @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum) + */ + public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000, float $multiplier = 1, int $maxDelayMilliseconds = 0) + { + $this->maxRetries = $maxRetries; + + if ($delayMilliseconds < 0) { + throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds)); + } + $this->delayMilliseconds = $delayMilliseconds; + + if ($multiplier < 1) { + throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier)); + } + $this->multiplier = $multiplier; + + if ($maxDelayMilliseconds < 0) { + throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds)); + } + $this->maxDelayMilliseconds = $maxDelayMilliseconds; + } + + /** + * @param \Throwable|null $throwable The cause of the failed handling + */ + public function isRetryable(Envelope $message, \Throwable $throwable = null): bool + { + $retries = RedeliveryStamp::getRetryCountFromEnvelope($message); + + return $retries < $this->maxRetries; + } + + /** + * @param \Throwable|null $throwable The cause of the failed handling + */ + public function getWaitingTime(Envelope $message, \Throwable $throwable = null): int + { + $retries = RedeliveryStamp::getRetryCountFromEnvelope($message); + + $delay = $this->delayMilliseconds * $this->multiplier ** $retries; + + if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) { + return $this->maxDelayMilliseconds; + } + + return (int) ceil($delay); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Retry/RetryStrategyInterface.php b/plugins/email/vendor/symfony/messenger/Retry/RetryStrategyInterface.php new file mode 100644 index 0000000..52c294b --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Retry/RetryStrategyInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Retry; + +use Symfony\Component\Messenger\Envelope; + +/** + * @author Fabien Potencier + * @author Grégoire Pineau + * @author Ryan Weaver + */ +interface RetryStrategyInterface +{ + /** + * @param \Throwable|null $throwable The cause of the failed handling + */ + public function isRetryable(Envelope $message/* , \Throwable $throwable = null */): bool; + + /** + * @param \Throwable|null $throwable The cause of the failed handling + * + * @return int The time to delay/wait in milliseconds + */ + public function getWaitingTime(Envelope $message/* , \Throwable $throwable = null */): int; +} diff --git a/plugins/email/vendor/symfony/messenger/RoutableMessageBus.php b/plugins/email/vendor/symfony/messenger/RoutableMessageBus.php new file mode 100644 index 0000000..ece1478 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/RoutableMessageBus.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Stamp\BusNameStamp; + +/** + * Bus of buses that is routable using a BusNameStamp. + * + * This is useful when passed to Worker: messages received + * from the transport can be sent to the correct bus. + * + * @author Ryan Weaver + */ +class RoutableMessageBus implements MessageBusInterface +{ + private $busLocator; + private $fallbackBus; + + public function __construct(ContainerInterface $busLocator, MessageBusInterface $fallbackBus = null) + { + $this->busLocator = $busLocator; + $this->fallbackBus = $fallbackBus; + } + + public function dispatch(object $envelope, array $stamps = []): Envelope + { + if (!$envelope instanceof Envelope) { + throw new InvalidArgumentException('Messages passed to RoutableMessageBus::dispatch() must be inside an Envelope.'); + } + + /** @var BusNameStamp|null $busNameStamp */ + $busNameStamp = $envelope->last(BusNameStamp::class); + + if (null === $busNameStamp) { + if (null === $this->fallbackBus) { + throw new InvalidArgumentException('Envelope is missing a BusNameStamp and no fallback message bus is configured on RoutableMessageBus.'); + } + + return $this->fallbackBus->dispatch($envelope, $stamps); + } + + return $this->getMessageBus($busNameStamp->getBusName())->dispatch($envelope, $stamps); + } + + /** + * @internal + */ + public function getMessageBus(string $busName): MessageBusInterface + { + if (!$this->busLocator->has($busName)) { + throw new InvalidArgumentException(sprintf('Bus named "%s" does not exist.', $busName)); + } + + return $this->busLocator->get($busName); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/AckStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/AckStamp.php new file mode 100644 index 0000000..b94c2c9 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/AckStamp.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\Messenger\Envelope; + +/** + * Marker stamp for messages that can be ack/nack'ed. + */ +final class AckStamp implements NonSendableStampInterface +{ + private $ack; + + /** + * @param \Closure(Envelope, \Throwable|null) $ack + */ + public function __construct(\Closure $ack) + { + $this->ack = $ack; + } + + public function ack(Envelope $envelope, \Throwable $e = null): void + { + ($this->ack)($envelope, $e); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/BusNameStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/BusNameStamp.php new file mode 100644 index 0000000..e9765c0 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/BusNameStamp.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Stamp used to identify which bus it was passed to. + * + * @author Ryan Weaver + */ +final class BusNameStamp implements StampInterface +{ + private $busName; + + public function __construct(string $busName) + { + $this->busName = $busName; + } + + public function getBusName(): string + { + return $this->busName; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/ConsumedByWorkerStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/ConsumedByWorkerStamp.php new file mode 100644 index 0000000..3ae37ba --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/ConsumedByWorkerStamp.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * A marker that this message was consumed by a worker process. + */ +class ConsumedByWorkerStamp implements NonSendableStampInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/DelayStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/DelayStamp.php new file mode 100644 index 0000000..92859e3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/DelayStamp.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Apply this stamp to delay delivery of your message on a transport. + */ +final class DelayStamp implements StampInterface +{ + private $delay; + + /** + * @param int $delay The delay in milliseconds + */ + public function __construct(int $delay) + { + $this->delay = $delay; + } + + public function getDelay(): int + { + return $this->delay; + } + + public static function delayFor(\DateInterval $interval): self + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + $end = $now->add($interval); + + return new self(($end->getTimestamp() - $now->getTimestamp()) * 1000); + } + + public static function delayUntil(\DateTimeInterface $dateTime): self + { + return new self(($dateTime->getTimestamp() - time()) * 1000); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/DispatchAfterCurrentBusStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/DispatchAfterCurrentBusStamp.php new file mode 100644 index 0000000..0ee31f0 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/DispatchAfterCurrentBusStamp.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Marker item to tell this message should be handled in after the current bus has finished. + * + * @see \Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware + * + * @author Tobias Nyholm + */ +final class DispatchAfterCurrentBusStamp implements NonSendableStampInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/ErrorDetailsStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/ErrorDetailsStamp.php new file mode 100644 index 0000000..62abf23 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/ErrorDetailsStamp.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Throwable; + +/** + * Stamp applied when a messages fails due to an exception in the handler. + */ +final class ErrorDetailsStamp implements StampInterface +{ + /** @var string */ + private $exceptionClass; + + /** @var int|string */ + private $exceptionCode; + + /** @var string */ + private $exceptionMessage; + + /** @var FlattenException|null */ + private $flattenException; + + /** + * @param int|string $exceptionCode + */ + public function __construct(string $exceptionClass, $exceptionCode, string $exceptionMessage, FlattenException $flattenException = null) + { + $this->exceptionClass = $exceptionClass; + $this->exceptionCode = $exceptionCode; + $this->exceptionMessage = $exceptionMessage; + $this->flattenException = $flattenException; + } + + public static function create(Throwable $throwable): self + { + if ($throwable instanceof HandlerFailedException) { + $throwable = $throwable->getPrevious(); + } + + $flattenException = null; + if (class_exists(FlattenException::class)) { + $flattenException = FlattenException::createFromThrowable($throwable); + } + + return new self(\get_class($throwable), $throwable->getCode(), $throwable->getMessage(), $flattenException); + } + + public function getExceptionClass(): string + { + return $this->exceptionClass; + } + + public function getExceptionCode() + { + return $this->exceptionCode; + } + + public function getExceptionMessage(): string + { + return $this->exceptionMessage; + } + + public function getFlattenException(): ?FlattenException + { + return $this->flattenException; + } + + public function equals(?self $that): bool + { + if (null === $that) { + return false; + } + + if ($this->flattenException && $that->flattenException) { + return $this->flattenException->getClass() === $that->flattenException->getClass() + && $this->flattenException->getCode() === $that->flattenException->getCode() + && $this->flattenException->getMessage() === $that->flattenException->getMessage(); + } + + return $this->exceptionClass === $that->exceptionClass + && $this->exceptionCode === $that->exceptionCode + && $this->exceptionMessage === $that->exceptionMessage; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/FlushBatchHandlersStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/FlushBatchHandlersStamp.php new file mode 100644 index 0000000..5dfbe22 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/FlushBatchHandlersStamp.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Marker telling that any batch handlers bound to the envelope should be flushed. + */ +final class FlushBatchHandlersStamp implements NonSendableStampInterface +{ + private $force; + + public function __construct(bool $force) + { + $this->force = $force; + } + + public function force(): bool + { + return $this->force; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/HandledStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/HandledStamp.php new file mode 100644 index 0000000..9d5a2c1 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/HandledStamp.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\Messenger\Handler\HandlerDescriptor; + +/** + * Stamp identifying a message handled by the `HandleMessageMiddleware` middleware + * and storing the handler returned value. + * + * This is used by synchronous command buses expecting a return value and the retry logic + * to only execute handlers that didn't succeed. + * + * @see \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware + * @see \Symfony\Component\Messenger\HandleTrait + * + * @author Maxime Steinhausser + */ +final class HandledStamp implements StampInterface +{ + private $result; + private $handlerName; + + /** + * @param mixed $result The returned value of the message handler + */ + public function __construct($result, string $handlerName) + { + $this->result = $result; + $this->handlerName = $handlerName; + } + + /** + * @param mixed $result The returned value of the message handler + */ + public static function fromDescriptor(HandlerDescriptor $handler, $result): self + { + return new self($result, $handler->getName()); + } + + /** + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + public function getHandlerName(): string + { + return $this->handlerName; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/NoAutoAckStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/NoAutoAckStamp.php new file mode 100644 index 0000000..15ba383 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/NoAutoAckStamp.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\Messenger\Handler\HandlerDescriptor; + +/** + * Marker telling that ack should not be done automatically for this message. + */ +final class NoAutoAckStamp implements NonSendableStampInterface +{ + private $handlerDescriptor; + + public function __construct(HandlerDescriptor $handlerDescriptor) + { + $this->handlerDescriptor = $handlerDescriptor; + } + + public function getHandlerDescriptor(): HandlerDescriptor + { + return $this->handlerDescriptor; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/NonSendableStampInterface.php b/plugins/email/vendor/symfony/messenger/Stamp/NonSendableStampInterface.php new file mode 100644 index 0000000..9ebd534 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/NonSendableStampInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * A stamp that should not be included with the Envelope if sent to a transport. + * + * @author Ryan Weaver + */ +interface NonSendableStampInterface extends StampInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/ReceivedStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/ReceivedStamp.php new file mode 100644 index 0000000..7297b17 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/ReceivedStamp.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; + +/** + * Marker stamp for a received message. + * + * This is mainly used by the `SendMessageMiddleware` middleware to identify + * a message should not be sent if it was just received. + * + * @see SendMessageMiddleware + * + * @author Samuel Roze + */ +final class ReceivedStamp implements NonSendableStampInterface +{ + private $transportName; + + public function __construct(string $transportName) + { + $this->transportName = $transportName; + } + + public function getTransportName(): string + { + return $this->transportName; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/RedeliveryStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/RedeliveryStamp.php new file mode 100644 index 0000000..fef9b06 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/RedeliveryStamp.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Messenger\Envelope; + +/** + * Stamp applied when a messages needs to be redelivered. + */ +final class RedeliveryStamp implements StampInterface +{ + private $retryCount; + private $redeliveredAt; + private $exceptionMessage; + private $flattenException; + + /** + * @param \DateTimeInterface|null $exceptionMessage + */ + public function __construct(int $retryCount, $exceptionMessage = null, FlattenException $flattenException = null, \DateTimeInterface $redeliveredAt = null) + { + $this->retryCount = $retryCount; + $this->redeliveredAt = $redeliveredAt ?? new \DateTimeImmutable(); + if (null !== $redeliveredAt) { + trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "$redeliveredAt" as 4th argument of the "%s::__construct()" is deprecated, pass "$redeliveredAt" as second argument instead.', self::class)); + } + + if ($exceptionMessage instanceof \DateTimeInterface) { + // In Symfony 6.0, the second argument will be $redeliveredAt + $this->redeliveredAt = $exceptionMessage; + if (null !== $redeliveredAt) { + throw new \LogicException('It is deprecated to specify a redeliveredAt as 4th argument. The correct way is to specify redeliveredAt as the second argument. Using both is not allowed.'); + } + } elseif (null !== $exceptionMessage) { + trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "$exceptionMessage" parameter in the "%s" class is deprecated, use the "%s" class instead.', self::class, ErrorDetailsStamp::class)); + $this->exceptionMessage = $exceptionMessage; + } + + if (null !== $flattenException) { + trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "$flattenException" parameter in the "%s" class is deprecated, use the "%s" class instead.', self::class, ErrorDetailsStamp::class)); + } + $this->flattenException = $flattenException; + } + + public static function getRetryCountFromEnvelope(Envelope $envelope): int + { + /** @var self|null $retryMessageStamp */ + $retryMessageStamp = $envelope->last(self::class); + + return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0; + } + + public function getRetryCount(): int + { + return $this->retryCount; + } + + /** + * @deprecated since Symfony 5.2, use ErrorDetailsStamp instead. + */ + public function getExceptionMessage(): ?string + { + trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "getExceptionMessage()" method of the "%s" class is deprecated, use the "%s" class instead.', self::class, ErrorDetailsStamp::class)); + + return $this->exceptionMessage; + } + + /** + * @deprecated since Symfony 5.2, use ErrorDetailsStamp instead. + */ + public function getFlattenException(): ?FlattenException + { + trigger_deprecation('symfony/messenger', '5.2', sprintf('Using the "getFlattenException()" method of the "%s" class is deprecated, use the "%s" class instead.', self::class, ErrorDetailsStamp::class)); + + return $this->flattenException; + } + + public function getRedeliveredAt(): \DateTimeInterface + { + return $this->redeliveredAt; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/RouterContextStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/RouterContextStamp.php new file mode 100644 index 0000000..4906f50 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/RouterContextStamp.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * @author Jérémy Derussé + */ +class RouterContextStamp implements StampInterface +{ + private $baseUrl; + private $method; + private $host; + private $scheme; + private $httpPort; + private $httpsPort; + private $pathInfo; + private $queryString; + + public function __construct(string $baseUrl, string $method, string $host, string $scheme, int $httpPort, int $httpsPort, string $pathInfo, string $queryString) + { + $this->baseUrl = $baseUrl; + $this->method = $method; + $this->host = $host; + $this->scheme = $scheme; + $this->httpPort = $httpPort; + $this->httpsPort = $httpsPort; + $this->pathInfo = $pathInfo; + $this->queryString = $queryString; + } + + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + public function getMethod(): string + { + return $this->method; + } + + public function getHost(): string + { + return $this->host; + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHttpPort(): int + { + return $this->httpPort; + } + + public function getHttpsPort(): int + { + return $this->httpsPort; + } + + public function getPathInfo(): string + { + return $this->pathInfo; + } + + public function getQueryString(): string + { + return $this->queryString; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/SentStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/SentStamp.php new file mode 100644 index 0000000..eebbfc3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/SentStamp.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Marker stamp identifying a message sent by the `SendMessageMiddleware`. + * + * @see \Symfony\Component\Messenger\Middleware\SendMessageMiddleware + * + * @author Maxime Steinhausser + */ +final class SentStamp implements NonSendableStampInterface +{ + private $senderClass; + private $senderAlias; + + public function __construct(string $senderClass, string $senderAlias = null) + { + $this->senderAlias = $senderAlias; + $this->senderClass = $senderClass; + } + + public function getSenderClass(): string + { + return $this->senderClass; + } + + public function getSenderAlias(): ?string + { + return $this->senderAlias; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/SentToFailureTransportStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/SentToFailureTransportStamp.php new file mode 100644 index 0000000..60810cf --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/SentToFailureTransportStamp.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Stamp applied when a message is sent to the failure transport. + * + * @author Ryan Weaver + */ +final class SentToFailureTransportStamp implements StampInterface +{ + private $originalReceiverName; + + public function __construct(string $originalReceiverName) + { + $this->originalReceiverName = $originalReceiverName; + } + + public function getOriginalReceiverName(): string + { + return $this->originalReceiverName; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/SerializerStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/SerializerStamp.php new file mode 100644 index 0000000..3df15ca --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/SerializerStamp.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * @author Maxime Steinhausser + */ +final class SerializerStamp implements StampInterface +{ + private $context; + + public function __construct(array $context) + { + $this->context = $context; + } + + public function getContext(): array + { + return $this->context; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/StampInterface.php b/plugins/email/vendor/symfony/messenger/Stamp/StampInterface.php new file mode 100644 index 0000000..dc1fc0a --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/StampInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * An envelope stamp related to a message. + * + * Stamps must be serializable value objects for transport. + * + * @author Maxime Steinhausser + */ +interface StampInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/TransportMessageIdStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/TransportMessageIdStamp.php new file mode 100644 index 0000000..2128b46 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/TransportMessageIdStamp.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Added by a sender or receiver to indicate the id of this message in that transport. + * + * @author Ryan Weaver + */ +final class TransportMessageIdStamp implements StampInterface +{ + private $id; + + /** + * @param mixed $id some "identifier" of the message in a transport + */ + public function __construct($id) + { + $this->id = $id; + } + + public function getId() + { + return $this->id; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Stamp/ValidationStamp.php b/plugins/email/vendor/symfony/messenger/Stamp/ValidationStamp.php new file mode 100644 index 0000000..2127187 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Stamp/ValidationStamp.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +use Symfony\Component\Validator\Constraints\GroupSequence; + +/** + * @author Maxime Steinhausser + */ +final class ValidationStamp implements StampInterface +{ + private $groups; + + /** + * @param string[]|GroupSequence $groups + */ + public function __construct($groups) + { + $this->groups = $groups; + } + + public function getGroups() + { + return $this->groups; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Test/Middleware/MiddlewareTestCase.php b/plugins/email/vendor/symfony/messenger/Test/Middleware/MiddlewareTestCase.php new file mode 100644 index 0000000..08c3d6a --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Test/Middleware/MiddlewareTestCase.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Test\Middleware; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Middleware\StackInterface; +use Symfony\Component\Messenger\Middleware\StackMiddleware; + +/** + * @author Nicolas Grekas + */ +abstract class MiddlewareTestCase extends TestCase +{ + protected function getStackMock(bool $nextIsCalled = true) + { + if (!$nextIsCalled) { + $stack = $this->createMock(StackInterface::class); + $stack + ->expects($this->never()) + ->method('next') + ; + + return $stack; + } + + $nextMiddleware = $this->createMock(MiddlewareInterface::class); + $nextMiddleware + ->expects($this->once()) + ->method('handle') + ->willReturnCallback(function (Envelope $envelope, StackInterface $stack): Envelope { + return $envelope; + }) + ; + + return new StackMiddleware($nextMiddleware); + } + + protected function getThrowingStackMock(\Throwable $throwable = null) + { + $nextMiddleware = $this->createMock(MiddlewareInterface::class); + $nextMiddleware + ->expects($this->once()) + ->method('handle') + ->willThrowException($throwable ?? new \RuntimeException('Thrown from next middleware.')) + ; + + return new StackMiddleware($nextMiddleware); + } +} diff --git a/plugins/email/vendor/symfony/messenger/TraceableMessageBus.php b/plugins/email/vendor/symfony/messenger/TraceableMessageBus.php new file mode 100644 index 0000000..df595b0 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/TraceableMessageBus.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * @author Samuel Roze + */ +class TraceableMessageBus implements MessageBusInterface +{ + private $decoratedBus; + private $dispatchedMessages = []; + + public function __construct(MessageBusInterface $decoratedBus) + { + $this->decoratedBus = $decoratedBus; + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $message, array $stamps = []): Envelope + { + $envelope = Envelope::wrap($message, $stamps); + $context = [ + 'stamps' => array_merge([], ...array_values($envelope->all())), + 'message' => $envelope->getMessage(), + 'caller' => $this->getCaller(), + 'callTime' => microtime(true), + ]; + + try { + return $envelope = $this->decoratedBus->dispatch($message, $stamps); + } catch (\Throwable $e) { + $context['exception'] = $e; + + throw $e; + } finally { + $this->dispatchedMessages[] = $context + ['stamps_after_dispatch' => array_merge([], ...array_values($envelope->all()))]; + } + } + + public function getDispatchedMessages(): array + { + return $this->dispatchedMessages; + } + + public function reset() + { + $this->dispatchedMessages = []; + } + + private function getCaller(): array + { + $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 8); + + $file = $trace[1]['file'] ?? null; + $line = $trace[1]['line'] ?? null; + + $handleTraitFile = (new \ReflectionClass(HandleTrait::class))->getFileName(); + $found = false; + for ($i = 1; $i < 8; ++$i) { + if (isset($trace[$i]['file'], $trace[$i + 1]['file'], $trace[$i + 1]['line']) && $trace[$i]['file'] === $handleTraitFile) { + $file = $trace[$i + 1]['file']; + $line = $trace[$i + 1]['line']; + $found = true; + + break; + } + } + + for ($i = 2; $i < 8 && !$found; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dispatch' === $trace[$i]['function'] + && is_a($trace[$i]['class'], MessageBusInterface::class, true) + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < 8) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($trace[$i]['function'], 'call_user_func')) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + break; + } + } + break; + } + } + + $name = str_replace('\\', '/', (string) $file); + + return [ + 'name' => substr($name, strrpos($name, '/') + 1), + 'file' => $file, + 'line' => $line, + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpFactory.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpFactory.php new file mode 100644 index 0000000..27775b7 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpFactory as BridgeAmqpFactory; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpFactory::class, BridgeAmqpFactory::class); + +class_exists(BridgeAmqpFactory::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpFactory + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpReceivedStamp.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpReceivedStamp.php new file mode 100644 index 0000000..3ce9a1d --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpReceivedStamp.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceivedStamp as BridgeAmqpReceivedStamp; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated,use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpReceivedStamp::class, BridgeAmqpReceivedStamp::class); + +class_exists(BridgeAmqpReceivedStamp::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpReceivedStamp + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpReceiver.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpReceiver.php new file mode 100644 index 0000000..3b4b8f9 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpReceiver.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver as BridgeAmqpReceiver; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpReceiver::class, BridgeAmqpReceiver::class); + +class_exists(BridgeAmqpReceiver::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpReceiver + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpSender.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpSender.php new file mode 100644 index 0000000..f6e195d --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpSender.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender as BridgeAmqpSender; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpSender::class, BridgeAmqpSender::class); + +class_exists(BridgeAmqpSender::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpSender + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpStamp.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpStamp.php new file mode 100644 index 0000000..5c6f696 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpStamp.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp as BridgeAmqpStamp; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpStamp::class, BridgeAmqpStamp::class); + +class_exists(BridgeAmqpStamp::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpStamp + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpTransport.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpTransport.php new file mode 100644 index 0000000..e14dbfc --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpTransport.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransport as BridgeAmqpTransport; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpTransport::class, BridgeAmqpTransport::class); + +class_exists(BridgeAmqpTransport::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpTransport + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpTransportFactory.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpTransportFactory.php new file mode 100644 index 0000000..2b48968 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/AmqpTransportFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory as BridgeAmqpTransportFactory; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', AmqpTransportFactory::class, BridgeAmqpTransportFactory::class); + +class_exists(BridgeAmqpTransportFactory::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class AmqpTransportFactory + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/Connection.php b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/Connection.php new file mode 100644 index 0000000..1301ca9 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/AmqpExt/Connection.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection as BridgeConnection; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The AmqpExt transport has been moved to package "symfony/amqp-messenger" and will not be included by default in 6.0. Run "composer require symfony/amqp-messenger".', Connection::class, BridgeConnection::class); + +class_exists(BridgeConnection::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/amqp-messenger instead. + */ + class Connection + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Doctrine/Connection.php b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/Connection.php new file mode 100644 index 0000000..6f02bcc --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/Connection.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Doctrine; + +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection as BridgeConnection; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', Connection::class, BridgeConnection::class); + +class_exists(BridgeConnection::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead. + */ + class Connection + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineReceivedStamp.php b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineReceivedStamp.php new file mode 100644 index 0000000..6adb60a --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineReceivedStamp.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Doctrine; + +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp as BridgeDoctrineReceivedStamp; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineReceivedStamp::class, BridgeDoctrineReceivedStamp::class); + +class_exists(BridgeDoctrineReceivedStamp::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead. + */ + class DoctrineReceivedStamp + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineReceiver.php b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineReceiver.php new file mode 100644 index 0000000..ffe0d44 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineReceiver.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Doctrine; + +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver as BridgeDoctrineReceiver; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineReceiver::class, BridgeDoctrineReceiver::class); + +class_exists(BridgeDoctrineReceiver::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead. + */ + class DoctrineReceiver + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineSender.php b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineSender.php new file mode 100644 index 0000000..17aa795 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineSender.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Doctrine; + +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineSender as BridgeDoctrineSender; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineSender::class, BridgeDoctrineSender::class); + +class_exists(BridgeDoctrineSender::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead. + */ + class DoctrineSender + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineTransport.php b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineTransport.php new file mode 100644 index 0000000..6aa96ba --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineTransport.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Doctrine; + +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport as BridgeDoctrineTransport; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineTransport::class, BridgeDoctrineTransport::class); + +class_exists(BridgeDoctrineTransport::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead. + */ + class DoctrineTransport + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineTransportFactory.php b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineTransportFactory.php new file mode 100644 index 0000000..fe277d6 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Doctrine/DoctrineTransportFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Doctrine; + +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransportFactory as BridgeDoctrineTransportFactory; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Doctrine transport has been moved to package "symfony/doctrine-messenger" and will not be included by default in 6.0. Run "composer require symfony/doctrine-messenger".', DoctrineTransportFactory::class, BridgeDoctrineTransportFactory::class); + +class_exists(BridgeDoctrineTransportFactory::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/doctrine-messenger instead. + */ + class DoctrineTransportFactory + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/InMemoryTransport.php b/plugins/email/vendor/symfony/messenger/Transport/InMemoryTransport.php new file mode 100644 index 0000000..eedbb9c --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/InMemoryTransport.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Transport that stays in memory. Useful for testing purpose. + * + * @author Gary PEGEOT + */ +class InMemoryTransport implements TransportInterface, ResetInterface +{ + /** + * @var Envelope[] + */ + private $sent = []; + + /** + * @var Envelope[] + */ + private $acknowledged = []; + + /** + * @var Envelope[] + */ + private $rejected = []; + + /** + * @var Envelope[] + */ + private $queue = []; + + private $nextId = 1; + + /** + * @var SerializerInterface|null + */ + private $serializer; + + public function __construct(SerializerInterface $serializer = null) + { + $this->serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + return array_values($this->decode($this->queue)); + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + $this->acknowledged[] = $this->encode($envelope); + + if (!$transportMessageIdStamp = $envelope->last(TransportMessageIdStamp::class)) { + throw new LogicException('No TransportMessageIdStamp found on the Envelope.'); + } + + unset($this->queue[$transportMessageIdStamp->getId()]); + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + $this->rejected[] = $this->encode($envelope); + + if (!$transportMessageIdStamp = $envelope->last(TransportMessageIdStamp::class)) { + throw new LogicException('No TransportMessageIdStamp found on the Envelope.'); + } + + unset($this->queue[$transportMessageIdStamp->getId()]); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + $id = $this->nextId++; + $envelope = $envelope->with(new TransportMessageIdStamp($id)); + $encodedEnvelope = $this->encode($envelope); + $this->sent[] = $encodedEnvelope; + $this->queue[$id] = $encodedEnvelope; + + return $envelope; + } + + public function reset() + { + $this->sent = $this->queue = $this->rejected = $this->acknowledged = []; + } + + /** + * @return Envelope[] + */ + public function getAcknowledged(): array + { + return $this->decode($this->acknowledged); + } + + /** + * @return Envelope[] + */ + public function getRejected(): array + { + return $this->decode($this->rejected); + } + + /** + * @return Envelope[] + */ + public function getSent(): array + { + return $this->decode($this->sent); + } + + /** + * @return Envelope|array + */ + private function encode(Envelope $envelope) + { + if (null === $this->serializer) { + return $envelope; + } + + return $this->serializer->encode($envelope); + } + + /** + * @param array $messagesEncoded + * + * @return Envelope[] + */ + private function decode(array $messagesEncoded): array + { + if (null === $this->serializer) { + return $messagesEncoded; + } + + return array_map( + [$this->serializer, 'decode'], + $messagesEncoded + ); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/InMemoryTransportFactory.php b/plugins/email/vendor/symfony/messenger/Transport/InMemoryTransportFactory.php new file mode 100644 index 0000000..ee7d028 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/InMemoryTransportFactory.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * @author Gary PEGEOT + */ +class InMemoryTransportFactory implements TransportFactoryInterface, ResetInterface +{ + /** + * @var InMemoryTransport[] + */ + private $createdTransports = []; + + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + ['serialize' => $serialize] = $this->parseDsn($dsn); + + return $this->createdTransports[] = new InMemoryTransport($serialize ? $serializer : null); + } + + public function supports(string $dsn, array $options): bool + { + return str_starts_with($dsn, 'in-memory://'); + } + + public function reset() + { + foreach ($this->createdTransports as $transport) { + $transport->reset(); + } + } + + private function parseDsn(string $dsn): array + { + $query = []; + if ($queryAsString = strstr($dsn, '?')) { + parse_str(ltrim($queryAsString, '?'), $query); + } + + return [ + 'serialize' => filter_var($query['serialize'] ?? false, \FILTER_VALIDATE_BOOLEAN), + ]; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Receiver/ListableReceiverInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Receiver/ListableReceiverInterface.php new file mode 100644 index 0000000..897c7a5 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Receiver/ListableReceiverInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Receiver; + +use Symfony\Component\Messenger\Envelope; + +/** + * Used when a receiver has the ability to list messages and find specific messages. + * A receiver that implements this should add the TransportMessageIdStamp + * to the Envelopes that it returns. + * + * @author Ryan Weaver + */ +interface ListableReceiverInterface extends ReceiverInterface +{ + /** + * Returns all the messages (up to the limit) in this receiver. + * + * Messages should be given the same stamps as when using ReceiverInterface::get(). + * + * @return Envelope[]|iterable + */ + public function all(int $limit = null): iterable; + + /** + * Returns the Envelope by id or none. + * + * Message should be given the same stamps as when using ReceiverInterface::get(). + */ + public function find($id): ?Envelope; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Receiver/MessageCountAwareInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Receiver/MessageCountAwareInterface.php new file mode 100644 index 0000000..b680d8a --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Receiver/MessageCountAwareInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Receiver; + +/** + * @author Samuel Roze + * @author Ryan Weaver + */ +interface MessageCountAwareInterface +{ + /** + * Returns the number of messages waiting to be handled. + * + * In some systems, this may be an approximate number. + */ + public function getMessageCount(): int; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Receiver/QueueReceiverInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Receiver/QueueReceiverInterface.php new file mode 100644 index 0000000..1886afe --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Receiver/QueueReceiverInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Receiver; + +use Symfony\Component\Messenger\Envelope; + +/** + * Some transports may have multiple queues. This interface is used to read from only some queues. + * + * @author David Buchmann + */ +interface QueueReceiverInterface extends ReceiverInterface +{ + /** + * Get messages from the specified queue names instead of consuming from all queues. + * + * @param string[] $queueNames + * + * @return Envelope[] + */ + public function getFromQueues(array $queueNames): iterable; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Receiver/ReceiverInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Receiver/ReceiverInterface.php new file mode 100644 index 0000000..68f72c5 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Receiver/ReceiverInterface.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Receiver; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\TransportException; + +/** + * @author Samuel Roze + * @author Ryan Weaver + */ +interface ReceiverInterface +{ + /** + * Receives some messages. + * + * While this method could return an unlimited number of messages, + * the intention is that it returns only one, or a "small number" + * of messages each time. This gives the user more flexibility: + * they can finish processing the one (or "small number") of messages + * from this receiver and move on to check other receivers for messages. + * If this method returns too many messages, it could cause a + * blocking effect where handling the messages received from one + * call to get() takes a long time, blocking other receivers from + * being called. + * + * If applicable, the Envelope should contain a TransportMessageIdStamp. + * + * If a received message cannot be decoded, the message should not + * be retried again (e.g. if there's a queue, it should be removed) + * and a MessageDecodingFailedException should be thrown. + * + * @throws TransportException If there is an issue communicating with the transport + * + * @return Envelope[] + */ + public function get(): iterable; + + /** + * Acknowledges that the passed message was handled. + * + * @throws TransportException If there is an issue communicating with the transport + */ + public function ack(Envelope $envelope): void; + + /** + * Called when handling the message failed and it should not be retried. + * + * @throws TransportException If there is an issue communicating with the transport + */ + public function reject(Envelope $envelope): void; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Receiver/SingleMessageReceiver.php b/plugins/email/vendor/symfony/messenger/Transport/Receiver/SingleMessageReceiver.php new file mode 100644 index 0000000..1ad5f22 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Receiver/SingleMessageReceiver.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Receiver; + +use Symfony\Component\Messenger\Envelope; + +/** + * Receiver that decorates another, but receives only 1 specific message. + * + * @author Ryan Weaver + * + * @internal + */ +class SingleMessageReceiver implements ReceiverInterface +{ + private $receiver; + private $envelope; + private $hasReceived = false; + + public function __construct(ReceiverInterface $receiver, Envelope $envelope) + { + $this->receiver = $receiver; + $this->envelope = $envelope; + } + + public function get(): iterable + { + if ($this->hasReceived) { + return []; + } + + $this->hasReceived = true; + + return [$this->envelope]; + } + + public function ack(Envelope $envelope): void + { + $this->receiver->ack($envelope); + } + + public function reject(Envelope $envelope): void + { + $this->receiver->reject($envelope); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/RedisExt/Connection.php b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/Connection.php new file mode 100644 index 0000000..39679ab --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/Connection.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\RedisExt; + +use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection as BridgeConnection; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', Connection::class, BridgeConnection::class); + +class_exists(BridgeConnection::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead. + */ + class Connection + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisReceivedStamp.php b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisReceivedStamp.php new file mode 100644 index 0000000..0a5866c --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisReceivedStamp.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\RedisExt; + +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisReceivedStamp as BridgeRedisReceivedStamp; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated,use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisReceivedStamp::class, BridgeRedisReceivedStamp::class); + +class_exists(BridgeRedisReceivedStamp::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead. + */ + class RedisReceivedStamp + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisReceiver.php b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisReceiver.php new file mode 100644 index 0000000..6aafbb1 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisReceiver.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\RedisExt; + +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisReceiver as BridgeRedisReceiver; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisReceiver::class, BridgeRedisReceiver::class); + +class_exists(BridgeRedisReceiver::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead. + */ + class RedisReceiver + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisSender.php b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisSender.php new file mode 100644 index 0000000..8928236 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisSender.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\RedisExt; + +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisSender as BridgeRedisSender; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisSender::class, BridgeRedisSender::class); + +class_exists(BridgeRedisSender::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead. + */ + class RedisSender + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisTransport.php b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisTransport.php new file mode 100644 index 0000000..939b748 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisTransport.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\RedisExt; + +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransport as BridgeRedisTransport; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisTransport::class, BridgeRedisTransport::class); + +class_exists(BridgeRedisTransport::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead. + */ + class RedisTransport + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisTransportFactory.php b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisTransportFactory.php new file mode 100644 index 0000000..a93822b --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/RedisExt/RedisTransportFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\RedisExt; + +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory as BridgeRedisTransportFactory; + +trigger_deprecation('symfony/messenger', '5.1', 'The "%s" class is deprecated, use "%s" instead. The RedisExt transport has been moved to package "symfony/redis-messenger" and will not be included by default in 6.0. Run "composer require symfony/redis-messenger".', RedisTransportFactory::class, BridgeRedisTransportFactory::class); + +class_exists(BridgeRedisTransportFactory::class); + +if (false) { + /** + * @deprecated since Symfony 5.1, to be removed in 6.0. Use symfony/redis-messenger instead. + */ + class RedisTransportFactory + { + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Sender/SenderInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Sender/SenderInterface.php new file mode 100644 index 0000000..3414a40 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Sender/SenderInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Sender; + +use Symfony\Component\Messenger\Envelope; + +/** + * @author Samuel Roze + */ +interface SenderInterface +{ + /** + * Sends the given envelope. + * + * The sender can read different stamps for transport configuration, + * like delivery delay. + * + * If applicable, the returned Envelope should contain a TransportMessageIdStamp. + */ + public function send(Envelope $envelope): Envelope; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Sender/SendersLocator.php b/plugins/email/vendor/symfony/messenger/Transport/Sender/SendersLocator.php new file mode 100644 index 0000000..9d0a1e3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Sender/SendersLocator.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Sender; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\RuntimeException; +use Symfony\Component\Messenger\Handler\HandlersLocator; + +/** + * Maps a message to a list of senders. + * + * @author Fabien Potencier + */ +class SendersLocator implements SendersLocatorInterface +{ + private $sendersMap; + private $sendersLocator; + + /** + * @param array> $sendersMap An array, keyed by "type", set to an array of sender aliases + * @param ContainerInterface $sendersLocator Locator of senders, keyed by sender alias + */ + public function __construct(array $sendersMap, ContainerInterface $sendersLocator) + { + $this->sendersMap = $sendersMap; + $this->sendersLocator = $sendersLocator; + } + + /** + * {@inheritdoc} + */ + public function getSenders(Envelope $envelope): iterable + { + $seen = []; + + foreach (HandlersLocator::listTypes($envelope) as $type) { + foreach ($this->sendersMap[$type] ?? [] as $senderAlias) { + if (!\in_array($senderAlias, $seen, true)) { + if (!$this->sendersLocator->has($senderAlias)) { + throw new RuntimeException(sprintf('Invalid senders configuration: sender "%s" is not in the senders locator.', $senderAlias)); + } + + $seen[] = $senderAlias; + $sender = $this->sendersLocator->get($senderAlias); + yield $senderAlias => $sender; + } + } + } + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Sender/SendersLocatorInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Sender/SendersLocatorInterface.php new file mode 100644 index 0000000..feab1f4 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Sender/SendersLocatorInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Sender; + +use Symfony\Component\Messenger\Envelope; + +/** + * Maps a message to a list of senders. + * + * @author Samuel Roze + * @author Tobias Schultze + */ +interface SendersLocatorInterface +{ + /** + * Gets the senders for the given message name. + * + * @return iterable Indexed by sender alias if available + */ + public function getSenders(Envelope $envelope): iterable; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php b/plugins/email/vendor/symfony/messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php new file mode 100644 index 0000000..5f335d0 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Serialization\Normalizer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Messenger\Transport\Serialization\Serializer; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; + +/** + * This normalizer is only used in Debug/Dev/Messenger contexts. + * + * @author Pascal Luna + */ +final class FlattenExceptionNormalizer implements DenormalizerInterface, ContextAwareNormalizerInterface +{ + use NormalizerAwareTrait; + + /** + * {@inheritdoc} + * + * @throws InvalidArgumentException + */ + public function normalize($object, string $format = null, array $context = []): array + { + $normalized = [ + 'message' => $object->getMessage(), + 'code' => $object->getCode(), + 'headers' => $object->getHeaders(), + 'class' => $object->getClass(), + 'file' => $object->getFile(), + 'line' => $object->getLine(), + 'previous' => null === $object->getPrevious() ? null : $this->normalize($object->getPrevious(), $format, $context), + 'status' => $object->getStatusCode(), + 'status_text' => $object->getStatusText(), + 'trace' => $object->getTrace(), + 'trace_as_string' => $object->getTraceAsString(), + ]; + + return $normalized; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, string $format = null, array $context = []): bool + { + return $data instanceof FlattenException && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false); + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, string $type, string $format = null, array $context = []): FlattenException + { + $object = new FlattenException(); + + $object->setMessage($data['message']); + $object->setCode($data['code']); + $object->setStatusCode($data['status'] ?? 500); + $object->setClass($data['class']); + $object->setFile($data['file']); + $object->setLine($data['line']); + $object->setStatusText($data['status_text']); + $object->setHeaders((array) $data['headers']); + + if (isset($data['previous'])) { + $object->setPrevious($this->denormalize($data['previous'], $type, $format, $context)); + } + + $property = new \ReflectionProperty(FlattenException::class, 'trace'); + $property->setAccessible(true); + $property->setValue($object, (array) $data['trace']); + + $property = new \ReflectionProperty(FlattenException::class, 'traceAsString'); + $property->setAccessible(true); + $property->setValue($object, $data['trace_as_string']); + + return $object; + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool + { + return FlattenException::class === $type && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Serialization/PhpSerializer.php b/plugins/email/vendor/symfony/messenger/Transport/Serialization/PhpSerializer.php new file mode 100644 index 0000000..e6332b3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Serialization/PhpSerializer.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Serialization; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; +use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; + +/** + * @author Ryan Weaver + */ +class PhpSerializer implements SerializerInterface +{ + /** + * {@inheritdoc} + */ + public function decode(array $encodedEnvelope): Envelope + { + if (empty($encodedEnvelope['body'])) { + throw new MessageDecodingFailedException('Encoded envelope should have at least a "body", or maybe you should implement your own serializer.'); + } + + if (!str_ends_with($encodedEnvelope['body'], '}')) { + $encodedEnvelope['body'] = base64_decode($encodedEnvelope['body']); + } + + $serializeEnvelope = stripslashes($encodedEnvelope['body']); + + return $this->safelyUnserialize($serializeEnvelope); + } + + /** + * {@inheritdoc} + */ + public function encode(Envelope $envelope): array + { + $envelope = $envelope->withoutStampsOfType(NonSendableStampInterface::class); + + $body = addslashes(serialize($envelope)); + + if (!preg_match('//u', $body)) { + $body = base64_encode($body); + } + + return [ + 'body' => $body, + ]; + } + + private function safelyUnserialize(string $contents) + { + if ('' === $contents) { + throw new MessageDecodingFailedException('Could not decode an empty message using PHP serialization.'); + } + + $signalingException = new MessageDecodingFailedException(sprintf('Could not decode message using PHP serialization: %s.', $contents)); + $prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback'); + $prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $signalingException) { + if (__FILE__ === $file) { + throw $signalingException; + } + + return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false; + }); + + try { + $meta = unserialize($contents); + } finally { + restore_error_handler(); + ini_set('unserialize_callback_func', $prevUnserializeHandler); + } + + return $meta; + } + + /** + * @internal + */ + public static function handleUnserializeCallback(string $class) + { + throw new MessageDecodingFailedException(sprintf('Message class "%s" not found during decoding.', $class)); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Serialization/Serializer.php b/plugins/email/vendor/symfony/messenger/Transport/Serialization/Serializer.php new file mode 100644 index 0000000..6142266 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Serialization/Serializer.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Serialization; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; +use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; +use Symfony\Component\Messenger\Stamp\SerializerStamp; +use Symfony\Component\Messenger\Stamp\StampInterface; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer as SymfonySerializer; +use Symfony\Component\Serializer\SerializerInterface as SymfonySerializerInterface; + +/** + * @author Samuel Roze + */ +class Serializer implements SerializerInterface +{ + public const MESSENGER_SERIALIZATION_CONTEXT = 'messenger_serialization'; + private const STAMP_HEADER_PREFIX = 'X-Message-Stamp-'; + + private $serializer; + private $format; + private $context; + + public function __construct(SymfonySerializerInterface $serializer = null, string $format = 'json', array $context = []) + { + $this->serializer = $serializer ?? self::create()->serializer; + $this->format = $format; + $this->context = $context + [self::MESSENGER_SERIALIZATION_CONTEXT => true]; + } + + public static function create(): self + { + if (!class_exists(SymfonySerializer::class)) { + throw new LogicException(sprintf('The "%s" class requires Symfony\'s Serializer component. Try running "composer require symfony/serializer" or use "%s" instead.', __CLASS__, PhpSerializer::class)); + } + + $encoders = [new XmlEncoder(), new JsonEncoder()]; + $normalizers = [new ArrayDenormalizer(), new ObjectNormalizer()]; + $serializer = new SymfonySerializer($normalizers, $encoders); + + return new self($serializer); + } + + /** + * {@inheritdoc} + */ + public function decode(array $encodedEnvelope): Envelope + { + if (empty($encodedEnvelope['body']) || empty($encodedEnvelope['headers'])) { + throw new MessageDecodingFailedException('Encoded envelope should have at least a "body" and some "headers", or maybe you should implement your own serializer.'); + } + + if (empty($encodedEnvelope['headers']['type'])) { + throw new MessageDecodingFailedException('Encoded envelope does not have a "type" header.'); + } + + $stamps = $this->decodeStamps($encodedEnvelope); + $serializerStamp = $this->findFirstSerializerStamp($stamps); + + $context = $this->context; + if (null !== $serializerStamp) { + $context = $serializerStamp->getContext() + $context; + } + + try { + $message = $this->serializer->deserialize($encodedEnvelope['body'], $encodedEnvelope['headers']['type'], $this->format, $context); + } catch (ExceptionInterface $e) { + throw new MessageDecodingFailedException('Could not decode message: '.$e->getMessage(), $e->getCode(), $e); + } + + return new Envelope($message, $stamps); + } + + /** + * {@inheritdoc} + */ + public function encode(Envelope $envelope): array + { + $context = $this->context; + /** @var SerializerStamp|null $serializerStamp */ + if ($serializerStamp = $envelope->last(SerializerStamp::class)) { + $context = $serializerStamp->getContext() + $context; + } + + $envelope = $envelope->withoutStampsOfType(NonSendableStampInterface::class); + + $headers = ['type' => \get_class($envelope->getMessage())] + $this->encodeStamps($envelope) + $this->getContentTypeHeader(); + + return [ + 'body' => $this->serializer->serialize($envelope->getMessage(), $this->format, $context), + 'headers' => $headers, + ]; + } + + private function decodeStamps(array $encodedEnvelope): array + { + $stamps = []; + foreach ($encodedEnvelope['headers'] as $name => $value) { + if (!str_starts_with($name, self::STAMP_HEADER_PREFIX)) { + continue; + } + + try { + $stamps[] = $this->serializer->deserialize($value, substr($name, \strlen(self::STAMP_HEADER_PREFIX)).'[]', $this->format, $this->context); + } catch (ExceptionInterface $e) { + throw new MessageDecodingFailedException('Could not decode stamp: '.$e->getMessage(), $e->getCode(), $e); + } + } + if ($stamps) { + $stamps = array_merge(...$stamps); + } + + return $stamps; + } + + private function encodeStamps(Envelope $envelope): array + { + if (!$allStamps = $envelope->all()) { + return []; + } + + $headers = []; + foreach ($allStamps as $class => $stamps) { + $headers[self::STAMP_HEADER_PREFIX.$class] = $this->serializer->serialize($stamps, $this->format, $this->context); + } + + return $headers; + } + + /** + * @param StampInterface[] $stamps + */ + private function findFirstSerializerStamp(array $stamps): ?SerializerStamp + { + foreach ($stamps as $stamp) { + if ($stamp instanceof SerializerStamp) { + return $stamp; + } + } + + return null; + } + + private function getContentTypeHeader(): array + { + $mimeType = $this->getMimeTypeForFormat(); + + return null === $mimeType ? [] : ['Content-Type' => $mimeType]; + } + + private function getMimeTypeForFormat(): ?string + { + switch ($this->format) { + case 'json': + return 'application/json'; + case 'xml': + return 'application/xml'; + case 'yml': + case 'yaml': + return 'application/x-yaml'; + case 'csv': + return 'text/csv'; + } + + return null; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Serialization/SerializerInterface.php b/plugins/email/vendor/symfony/messenger/Transport/Serialization/SerializerInterface.php new file mode 100644 index 0000000..fc133f7 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Serialization/SerializerInterface.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Serialization; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; + +/** + * @author Samuel Roze + */ +interface SerializerInterface +{ + /** + * Decodes an envelope and its message from an encoded-form. + * + * The `$encodedEnvelope` parameter is a key-value array that + * describes the envelope and its content, that will be used by the different transports. + * + * The most common keys are: + * - `body` (string) - the message body + * - `headers` (string) - a key/value pair of headers + * + * @throws MessageDecodingFailedException + */ + public function decode(array $encodedEnvelope): Envelope; + + /** + * Encodes an envelope content (message & stamps) to a common format understandable by transports. + * The encoded array should only contain scalars and arrays. + * + * Stamps that implement NonSendableStampInterface should + * not be encoded. + * + * The most common keys of the encoded array are: + * - `body` (string) - the message body + * - `headers` (string) - a key/value pair of headers + */ + public function encode(Envelope $envelope): array; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/SetupableTransportInterface.php b/plugins/email/vendor/symfony/messenger/Transport/SetupableTransportInterface.php new file mode 100644 index 0000000..8b6a857 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/SetupableTransportInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +/** + * @author Vincent Touzet + */ +interface SetupableTransportInterface +{ + /** + * Setup the transport. + */ + public function setup(): void; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Sync/SyncTransport.php b/plugins/email/vendor/symfony/messenger/Transport/Sync/SyncTransport.php new file mode 100644 index 0000000..67af903 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Sync/SyncTransport.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Sync; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\ReceivedStamp; +use Symfony\Component\Messenger\Stamp\SentStamp; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * Transport that immediately marks messages as received and dispatches for handling. + * + * @author Ryan Weaver + */ +class SyncTransport implements TransportInterface +{ + private $messageBus; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + public function get(): iterable + { + throw new InvalidArgumentException('You cannot receive messages from the Messenger SyncTransport.'); + } + + public function stop(): void + { + throw new InvalidArgumentException('You cannot call stop() on the Messenger SyncTransport.'); + } + + public function ack(Envelope $envelope): void + { + throw new InvalidArgumentException('You cannot call ack() on the Messenger SyncTransport.'); + } + + public function reject(Envelope $envelope): void + { + throw new InvalidArgumentException('You cannot call reject() on the Messenger SyncTransport.'); + } + + public function send(Envelope $envelope): Envelope + { + /** @var SentStamp|null $sentStamp */ + $sentStamp = $envelope->last(SentStamp::class); + $alias = null === $sentStamp ? 'sync' : ($sentStamp->getSenderAlias() ?: $sentStamp->getSenderClass()); + + $envelope = $envelope->with(new ReceivedStamp($alias)); + + return $this->messageBus->dispatch($envelope); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/Sync/SyncTransportFactory.php b/plugins/email/vendor/symfony/messenger/Transport/Sync/SyncTransportFactory.php new file mode 100644 index 0000000..d30c49c --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/Sync/SyncTransportFactory.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Sync; + +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\TransportFactoryInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Ryan Weaver + */ +class SyncTransportFactory implements TransportFactoryInterface +{ + private $messageBus; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + return new SyncTransport($this->messageBus); + } + + public function supports(string $dsn, array $options): bool + { + return str_starts_with($dsn, 'sync://'); + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/TransportFactory.php b/plugins/email/vendor/symfony/messenger/Transport/TransportFactory.php new file mode 100644 index 0000000..474dd6f --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/TransportFactory.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * @author Samuel Roze + */ +class TransportFactory implements TransportFactoryInterface +{ + private $factories; + + /** + * @param iterable $factories + */ + public function __construct(iterable $factories) + { + $this->factories = $factories; + } + + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn, $options)) { + return $factory->createTransport($dsn, $options, $serializer); + } + } + + // Help the user to select Symfony packages based on protocol. + $packageSuggestion = ''; + if (0 === strpos($dsn, 'amqp://')) { + $packageSuggestion = ' Run "composer require symfony/amqp-messenger" to install AMQP transport.'; + } elseif (0 === strpos($dsn, 'doctrine://')) { + $packageSuggestion = ' Run "composer require symfony/doctrine-messenger" to install Doctrine transport.'; + } elseif (0 === strpos($dsn, 'redis://') || 0 === strpos($dsn, 'rediss://')) { + $packageSuggestion = ' Run "composer require symfony/redis-messenger" to install Redis transport.'; + } elseif (0 === strpos($dsn, 'sqs://') || preg_match('#^https://sqs\.[\w\-]+\.amazonaws\.com/.+#', $dsn)) { + $packageSuggestion = ' Run "composer require symfony/amazon-sqs-messenger" to install Amazon SQS transport.'; + } elseif (0 === strpos($dsn, 'beanstalkd://')) { + $packageSuggestion = ' Run "composer require symfony/beanstalkd-messenger" to install Beanstalkd transport.'; + } + + throw new InvalidArgumentException(sprintf('No transport supports the given Messenger DSN "%s".%s.', $dsn, $packageSuggestion)); + } + + public function supports(string $dsn, array $options): bool + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn, $options)) { + return true; + } + } + + return false; + } +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/TransportFactoryInterface.php b/plugins/email/vendor/symfony/messenger/Transport/TransportFactoryInterface.php new file mode 100644 index 0000000..5741c30 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/TransportFactoryInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * Creates a Messenger transport. + * + * @author Samuel Roze + */ +interface TransportFactoryInterface +{ + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface; + + public function supports(string $dsn, array $options): bool; +} diff --git a/plugins/email/vendor/symfony/messenger/Transport/TransportInterface.php b/plugins/email/vendor/symfony/messenger/Transport/TransportInterface.php new file mode 100644 index 0000000..18c1bb8 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Transport/TransportInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; +use Symfony\Component\Messenger\Transport\Sender\SenderInterface; + +/** + * @author Nicolas Grekas + */ +interface TransportInterface extends ReceiverInterface, SenderInterface +{ +} diff --git a/plugins/email/vendor/symfony/messenger/Worker.php b/plugins/email/vendor/symfony/messenger/Worker.php new file mode 100644 index 0000000..33358d3 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/Worker.php @@ -0,0 +1,268 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; +use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; +use Symfony\Component\Messenger\Event\WorkerRunningEvent; +use Symfony\Component\Messenger\Event\WorkerStartedEvent; +use Symfony\Component\Messenger\Event\WorkerStoppedEvent; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException; +use Symfony\Component\Messenger\Exception\RuntimeException; +use Symfony\Component\Messenger\Stamp\AckStamp; +use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; +use Symfony\Component\Messenger\Stamp\FlushBatchHandlersStamp; +use Symfony\Component\Messenger\Stamp\NoAutoAckStamp; +use Symfony\Component\Messenger\Stamp\ReceivedStamp; +use Symfony\Component\Messenger\Transport\Receiver\QueueReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * @author Samuel Roze + * @author Tobias Schultze + * + * @final + */ +class Worker +{ + private $receivers; + private $bus; + private $eventDispatcher; + private $logger; + private $shouldStop = false; + private $metadata; + private $acks = []; + private $unacks; + + /** + * @param ReceiverInterface[] $receivers Where the key is the transport name + */ + public function __construct(array $receivers, MessageBusInterface $bus, EventDispatcherInterface $eventDispatcher = null, LoggerInterface $logger = null) + { + $this->receivers = $receivers; + $this->bus = $bus; + $this->logger = $logger; + $this->eventDispatcher = class_exists(Event::class) ? LegacyEventDispatcherProxy::decorate($eventDispatcher) : $eventDispatcher; + $this->metadata = new WorkerMetadata([ + 'transportNames' => array_keys($receivers), + ]); + $this->unacks = new \SplObjectStorage(); + } + + /** + * Receive the messages and dispatch them to the bus. + * + * Valid options are: + * * sleep (default: 1000000): Time in microseconds to sleep after no messages are found + * * queues: The queue names to consume from, instead of consuming from all queues. When this is used, all receivers must implement the QueueReceiverInterface + */ + public function run(array $options = []): void + { + $options = array_merge([ + 'sleep' => 1000000, + ], $options); + $queueNames = $options['queues'] ?? null; + + $this->metadata->set(['queueNames' => $queueNames]); + + $this->dispatchEvent(new WorkerStartedEvent($this)); + + if ($queueNames) { + // if queue names are specified, all receivers must implement the QueueReceiverInterface + foreach ($this->receivers as $transportName => $receiver) { + if (!$receiver instanceof QueueReceiverInterface) { + throw new RuntimeException(sprintf('Receiver for "%s" does not implement "%s".', $transportName, QueueReceiverInterface::class)); + } + } + } + + while (!$this->shouldStop) { + $envelopeHandled = false; + $envelopeHandledStart = microtime(true); + foreach ($this->receivers as $transportName => $receiver) { + if ($queueNames) { + $envelopes = $receiver->getFromQueues($queueNames); + } else { + $envelopes = $receiver->get(); + } + + foreach ($envelopes as $envelope) { + $envelopeHandled = true; + + $this->handleMessage($envelope, $transportName); + $this->dispatchEvent(new WorkerRunningEvent($this, false)); + + if ($this->shouldStop) { + break 2; + } + } + + // after handling a single receiver, quit and start the loop again + // this should prevent multiple lower priority receivers from + // blocking too long before the higher priority are checked + if ($envelopeHandled) { + break; + } + } + + if (!$envelopeHandled && $this->flush(false)) { + continue; + } + + if (!$envelopeHandled) { + $this->dispatchEvent(new WorkerRunningEvent($this, true)); + + if (0 < $sleep = (int) ($options['sleep'] - 1e6 * (microtime(true) - $envelopeHandledStart))) { + usleep($sleep); + } + } + } + + $this->flush(true); + $this->dispatchEvent(new WorkerStoppedEvent($this)); + } + + private function handleMessage(Envelope $envelope, string $transportName): void + { + $event = new WorkerMessageReceivedEvent($envelope, $transportName); + $this->dispatchEvent($event); + $envelope = $event->getEnvelope(); + + if (!$event->shouldHandle()) { + return; + } + + $acked = false; + $ack = function (Envelope $envelope, \Throwable $e = null) use ($transportName, &$acked) { + $acked = true; + $this->acks[] = [$transportName, $envelope, $e]; + }; + + try { + $e = null; + $envelope = $this->bus->dispatch($envelope->with(new ReceivedStamp($transportName), new ConsumedByWorkerStamp(), new AckStamp($ack))); + } catch (\Throwable $e) { + } + + $noAutoAckStamp = $envelope->last(NoAutoAckStamp::class); + + if (!$acked && !$noAutoAckStamp) { + $this->acks[] = [$transportName, $envelope, $e]; + } elseif ($noAutoAckStamp) { + $this->unacks[$noAutoAckStamp->getHandlerDescriptor()->getBatchHandler()] = [$envelope->withoutAll(AckStamp::class), $transportName]; + } + + $this->ack(); + } + + private function ack(): bool + { + $acks = $this->acks; + $this->acks = []; + + foreach ($acks as [$transportName, $envelope, $e]) { + $receiver = $this->receivers[$transportName]; + + if (null !== $e) { + if ($rejectFirst = $e instanceof RejectRedeliveredMessageException) { + // redelivered messages are rejected first so that continuous failures in an event listener or while + // publishing for retry does not cause infinite redelivery loops + $receiver->reject($envelope); + } + + if ($e instanceof HandlerFailedException) { + $envelope = $e->getEnvelope(); + } + + $failedEvent = new WorkerMessageFailedEvent($envelope, $transportName, $e); + + $this->dispatchEvent($failedEvent); + $envelope = $failedEvent->getEnvelope(); + + if (!$rejectFirst) { + $receiver->reject($envelope); + } + + continue; + } + + $handledEvent = new WorkerMessageHandledEvent($envelope, $transportName); + $this->dispatchEvent($handledEvent); + $envelope = $handledEvent->getEnvelope(); + + if (null !== $this->logger) { + $message = $envelope->getMessage(); + $context = [ + 'class' => \get_class($message), + ]; + $this->logger->info('{class} was handled successfully (acknowledging to transport).', $context); + } + + $receiver->ack($envelope); + } + + return (bool) $acks; + } + + private function flush(bool $force): bool + { + $unacks = $this->unacks; + + if (!$unacks->count()) { + return false; + } + + $this->unacks = new \SplObjectStorage(); + + foreach ($unacks as $batchHandler) { + [$envelope, $transportName] = $unacks[$batchHandler]; + try { + $this->bus->dispatch($envelope->with(new FlushBatchHandlersStamp($force))); + $envelope = $envelope->withoutAll(NoAutoAckStamp::class); + unset($unacks[$batchHandler], $batchHandler); + } catch (\Throwable $e) { + $this->acks[] = [$transportName, $envelope, $e]; + } + } + + return $this->ack(); + } + + public function stop(): void + { + if (null !== $this->logger) { + $this->logger->info('Stopping worker.', ['transport_names' => $this->metadata->getTransportNames()]); + } + + $this->shouldStop = true; + } + + public function getMetadata(): WorkerMetadata + { + return $this->metadata; + } + + private function dispatchEvent(object $event): void + { + if (null === $this->eventDispatcher) { + return; + } + + $this->eventDispatcher->dispatch($event); + } +} diff --git a/plugins/email/vendor/symfony/messenger/WorkerMetadata.php b/plugins/email/vendor/symfony/messenger/WorkerMetadata.php new file mode 100644 index 0000000..0eaab06 --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/WorkerMetadata.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * @author Oleg Krasavin + */ +final class WorkerMetadata +{ + private $metadata; + + public function __construct(array $metadata) + { + $this->metadata = $metadata; + } + + public function set(array $newMetadata): void + { + $this->metadata = array_merge($this->metadata, $newMetadata); + } + + /** + * Returns the queue names the worker consumes from, if "--queues" option was used. + * Returns null otherwise. + */ + public function getQueueNames(): ?array + { + return $this->metadata['queueNames'] ?? null; + } + + /** + * Returns an array of unique identifiers for transport receivers the worker consumes from. + */ + public function getTransportNames(): array + { + return $this->metadata['transportNames'] ?? []; + } +} diff --git a/plugins/email/vendor/symfony/messenger/composer.json b/plugins/email/vendor/symfony/messenger/composer.json new file mode 100644 index 0000000..a62537c --- /dev/null +++ b/plugins/email/vendor/symfony/messenger/composer.json @@ -0,0 +1,57 @@ +{ + "name": "symfony/messenger", + "type": "library", + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/log": "^1|^2|^3", + "symfony/amqp-messenger": "^5.1|^6.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/doctrine-messenger": "^5.1|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/redis-messenger": "^5.1|^6.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/serializer": "^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/validator": "^4.4|^5.0|^6.0" + }, + "conflict": { + "symfony/event-dispatcher": "<4.4", + "symfony/framework-bundle": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/serializer": "<5.0" + }, + "suggest": { + "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Messenger\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/mime/Address.php b/plugins/email/vendor/symfony/mime/Address.php new file mode 100644 index 0000000..ae83efd --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Address.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Egulias\EmailValidator\EmailValidator; +use Egulias\EmailValidator\Validation\MessageIDValidation; +use Egulias\EmailValidator\Validation\RFCValidation; +use Symfony\Component\Mime\Encoder\IdnAddressEncoder; +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * @author Fabien Potencier + */ +final class Address +{ + /** + * A regex that matches a structure like 'Name '. + * It matches anything between the first < and last > as email address. + * This allows to use a single string to construct an Address, which can be convenient to use in + * config, and allows to have more readable config. + * This does not try to cover all edge cases for address. + */ + private const FROM_STRING_PATTERN = '~(?[^<]*)<(?.*)>[^>]*~'; + + private static $validator; + private static $encoder; + + private $address; + private $name; + + public function __construct(string $address, string $name = '') + { + if (!class_exists(EmailValidator::class)) { + throw new LogicException(sprintf('The "%s" class cannot be used as it needs "%s"; try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class)); + } + + if (null === self::$validator) { + self::$validator = new EmailValidator(); + } + + $this->address = trim($address); + $this->name = trim(str_replace(["\n", "\r"], '', $name)); + + if (!self::$validator->isValid($this->address, class_exists(MessageIDValidation::class) ? new MessageIDValidation() : new RFCValidation())) { + throw new RfcComplianceException(sprintf('Email "%s" does not comply with addr-spec of RFC 2822.', $address)); + } + } + + public function getAddress(): string + { + return $this->address; + } + + public function getName(): string + { + return $this->name; + } + + public function getEncodedAddress(): string + { + if (null === self::$encoder) { + self::$encoder = new IdnAddressEncoder(); + } + + return self::$encoder->encodeString($this->address); + } + + public function toString(): string + { + return ($n = $this->getEncodedName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress(); + } + + public function getEncodedName(): string + { + if ('' === $this->getName()) { + return ''; + } + + return sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName())); + } + + /** + * @param Address|string $address + */ + public static function create($address): self + { + if ($address instanceof self) { + return $address; + } + + if (!\is_string($address)) { + throw new InvalidArgumentException(sprintf('An address can be an instance of Address or a string ("%s" given).', get_debug_type($address))); + } + + if (false === strpos($address, '<')) { + return new self($address); + } + + if (!preg_match(self::FROM_STRING_PATTERN, $address, $matches)) { + throw new InvalidArgumentException(sprintf('Could not parse "%s" to a "%s" instance.', $address, self::class)); + } + + return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"')); + } + + /** + * @param array $addresses + * + * @return Address[] + */ + public static function createArray(array $addresses): array + { + $addrs = []; + foreach ($addresses as $address) { + $addrs[] = self::create($address); + } + + return $addrs; + } + + /** + * @deprecated since Symfony 5.2, use "create()" instead. + */ + public static function fromString(string $string): self + { + trigger_deprecation('symfony/mime', '5.2', '"%s()" is deprecated, use "%s::create()" instead.', __METHOD__, __CLASS__); + + if (!str_contains($string, '<')) { + return new self($string, ''); + } + + if (!preg_match(self::FROM_STRING_PATTERN, $string, $matches)) { + throw new InvalidArgumentException(sprintf('Could not parse "%s" to a "%s" instance.', $string, self::class)); + } + + return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"')); + } +} diff --git a/plugins/email/vendor/symfony/mime/BodyRendererInterface.php b/plugins/email/vendor/symfony/mime/BodyRendererInterface.php new file mode 100644 index 0000000..d692172 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/BodyRendererInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * @author Fabien Potencier + */ +interface BodyRendererInterface +{ + public function render(Message $message): void; +} diff --git a/plugins/email/vendor/symfony/mime/CHANGELOG.md b/plugins/email/vendor/symfony/mime/CHANGELOG.md new file mode 100644 index 0000000..f272346 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/CHANGELOG.md @@ -0,0 +1,26 @@ +CHANGELOG +========= + +5.2.0 +----- + + * Add support for DKIM + * Deprecated `Address::fromString()`, use `Address::create()` instead + +4.4.0 +----- + + * [BC BREAK] Removed `NamedAddress` (`Address` now supports a name) + * Added PHPUnit constraints + * Added `AbstractPart::asDebugString()` + * Added `Address::fromString()` + +4.3.3 +----- + + * [BC BREAK] Renamed method `Headers::getAll()` to `Headers::all()`. + +4.3.0 +----- + + * Introduced the component as experimental diff --git a/plugins/email/vendor/symfony/mime/CharacterStream.php b/plugins/email/vendor/symfony/mime/CharacterStream.php new file mode 100644 index 0000000..238debd --- /dev/null +++ b/plugins/email/vendor/symfony/mime/CharacterStream.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * @author Fabien Potencier + * @author Xavier De Cock + * + * @internal + */ +final class CharacterStream +{ + /** Pre-computed for optimization */ + private const UTF8_LENGTH_MAP = [ + "\x00" => 1, "\x01" => 1, "\x02" => 1, "\x03" => 1, "\x04" => 1, "\x05" => 1, "\x06" => 1, "\x07" => 1, + "\x08" => 1, "\x09" => 1, "\x0a" => 1, "\x0b" => 1, "\x0c" => 1, "\x0d" => 1, "\x0e" => 1, "\x0f" => 1, + "\x10" => 1, "\x11" => 1, "\x12" => 1, "\x13" => 1, "\x14" => 1, "\x15" => 1, "\x16" => 1, "\x17" => 1, + "\x18" => 1, "\x19" => 1, "\x1a" => 1, "\x1b" => 1, "\x1c" => 1, "\x1d" => 1, "\x1e" => 1, "\x1f" => 1, + "\x20" => 1, "\x21" => 1, "\x22" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, + "\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1, + "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1, + "\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1, + "\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1, + "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, + "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1, + "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5c" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1, + "\x60" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, + "\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, + "\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, + "\x78" => 1, "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x7f" => 1, + "\x80" => 0, "\x81" => 0, "\x82" => 0, "\x83" => 0, "\x84" => 0, "\x85" => 0, "\x86" => 0, "\x87" => 0, + "\x88" => 0, "\x89" => 0, "\x8a" => 0, "\x8b" => 0, "\x8c" => 0, "\x8d" => 0, "\x8e" => 0, "\x8f" => 0, + "\x90" => 0, "\x91" => 0, "\x92" => 0, "\x93" => 0, "\x94" => 0, "\x95" => 0, "\x96" => 0, "\x97" => 0, + "\x98" => 0, "\x99" => 0, "\x9a" => 0, "\x9b" => 0, "\x9c" => 0, "\x9d" => 0, "\x9e" => 0, "\x9f" => 0, + "\xa0" => 0, "\xa1" => 0, "\xa2" => 0, "\xa3" => 0, "\xa4" => 0, "\xa5" => 0, "\xa6" => 0, "\xa7" => 0, + "\xa8" => 0, "\xa9" => 0, "\xaa" => 0, "\xab" => 0, "\xac" => 0, "\xad" => 0, "\xae" => 0, "\xaf" => 0, + "\xb0" => 0, "\xb1" => 0, "\xb2" => 0, "\xb3" => 0, "\xb4" => 0, "\xb5" => 0, "\xb6" => 0, "\xb7" => 0, + "\xb8" => 0, "\xb9" => 0, "\xba" => 0, "\xbb" => 0, "\xbc" => 0, "\xbd" => 0, "\xbe" => 0, "\xbf" => 0, + "\xc0" => 2, "\xc1" => 2, "\xc2" => 2, "\xc3" => 2, "\xc4" => 2, "\xc5" => 2, "\xc6" => 2, "\xc7" => 2, + "\xc8" => 2, "\xc9" => 2, "\xca" => 2, "\xcb" => 2, "\xcc" => 2, "\xcd" => 2, "\xce" => 2, "\xcf" => 2, + "\xd0" => 2, "\xd1" => 2, "\xd2" => 2, "\xd3" => 2, "\xd4" => 2, "\xd5" => 2, "\xd6" => 2, "\xd7" => 2, + "\xd8" => 2, "\xd9" => 2, "\xda" => 2, "\xdb" => 2, "\xdc" => 2, "\xdd" => 2, "\xde" => 2, "\xdf" => 2, + "\xe0" => 3, "\xe1" => 3, "\xe2" => 3, "\xe3" => 3, "\xe4" => 3, "\xe5" => 3, "\xe6" => 3, "\xe7" => 3, + "\xe8" => 3, "\xe9" => 3, "\xea" => 3, "\xeb" => 3, "\xec" => 3, "\xed" => 3, "\xee" => 3, "\xef" => 3, + "\xf0" => 4, "\xf1" => 4, "\xf2" => 4, "\xf3" => 4, "\xf4" => 4, "\xf5" => 4, "\xf6" => 4, "\xf7" => 4, + "\xf8" => 5, "\xf9" => 5, "\xfa" => 5, "\xfb" => 5, "\xfc" => 6, "\xfd" => 6, "\xfe" => 0, "\xff" => 0, + ]; + + private $data = ''; + private $dataSize = 0; + private $map = []; + private $charCount = 0; + private $currentPos = 0; + private $fixedWidth = 0; + + /** + * @param resource|string $input + */ + public function __construct($input, ?string $charset = 'utf-8') + { + $charset = strtolower(trim($charset)) ?: 'utf-8'; + if ('utf-8' === $charset || 'utf8' === $charset) { + $this->fixedWidth = 0; + $this->map = ['p' => [], 'i' => []]; + } else { + switch ($charset) { + // 16 bits + case 'ucs2': + case 'ucs-2': + case 'utf16': + case 'utf-16': + $this->fixedWidth = 2; + break; + + // 32 bits + case 'ucs4': + case 'ucs-4': + case 'utf32': + case 'utf-32': + $this->fixedWidth = 4; + break; + + // 7-8 bit charsets: (us-)?ascii, (iso|iec)-?8859-?[0-9]+, windows-?125[0-9], cp-?[0-9]+, ansi, macintosh, + // koi-?7, koi-?8-?.+, mik, (cork|t1), v?iscii + // and fallback + default: + $this->fixedWidth = 1; + } + } + if (\is_resource($input)) { + $blocks = 16372; + while (false !== $read = fread($input, $blocks)) { + $this->write($read); + } + } else { + $this->write($input); + } + } + + public function read(int $length): ?string + { + if ($this->currentPos >= $this->charCount) { + return null; + } + $length = ($this->currentPos + $length > $this->charCount) ? $this->charCount - $this->currentPos : $length; + if ($this->fixedWidth > 0) { + $len = $length * $this->fixedWidth; + $ret = substr($this->data, $this->currentPos * $this->fixedWidth, $len); + $this->currentPos += $length; + } else { + $end = $this->currentPos + $length; + $end = $end > $this->charCount ? $this->charCount : $end; + $ret = ''; + $start = 0; + if ($this->currentPos > 0) { + $start = $this->map['p'][$this->currentPos - 1]; + } + $to = $start; + for (; $this->currentPos < $end; ++$this->currentPos) { + if (isset($this->map['i'][$this->currentPos])) { + $ret .= substr($this->data, $start, $to - $start).'?'; + $start = $this->map['p'][$this->currentPos]; + } else { + $to = $this->map['p'][$this->currentPos]; + } + } + $ret .= substr($this->data, $start, $to - $start); + } + + return $ret; + } + + public function readBytes(int $length): ?array + { + if (null !== $read = $this->read($length)) { + return array_map('ord', str_split($read, 1)); + } + + return null; + } + + public function setPointer(int $charOffset): void + { + if ($this->charCount < $charOffset) { + $charOffset = $this->charCount; + } + $this->currentPos = $charOffset; + } + + public function write(string $chars): void + { + $ignored = ''; + $this->data .= $chars; + if ($this->fixedWidth > 0) { + $strlen = \strlen($chars); + $ignoredL = $strlen % $this->fixedWidth; + $ignored = $ignoredL ? substr($chars, -$ignoredL) : ''; + $this->charCount += ($strlen - $ignoredL) / $this->fixedWidth; + } else { + $this->charCount += $this->getUtf8CharPositions($chars, $this->dataSize, $ignored); + } + $this->dataSize = \strlen($this->data) - \strlen($ignored); + } + + private function getUtf8CharPositions(string $string, int $startOffset, string &$ignoredChars): int + { + $strlen = \strlen($string); + $charPos = \count($this->map['p']); + $foundChars = 0; + $invalid = false; + for ($i = 0; $i < $strlen; ++$i) { + $char = $string[$i]; + $size = self::UTF8_LENGTH_MAP[$char]; + if (0 == $size) { + /* char is invalid, we must wait for a resync */ + $invalid = true; + continue; + } + + if ($invalid) { + /* We mark the chars as invalid and start a new char */ + $this->map['p'][$charPos + $foundChars] = $startOffset + $i; + $this->map['i'][$charPos + $foundChars] = true; + ++$foundChars; + $invalid = false; + } + if (($i + $size) > $strlen) { + $ignoredChars = substr($string, $i); + break; + } + for ($j = 1; $j < $size; ++$j) { + $char = $string[$i + $j]; + if ($char > "\x7F" && $char < "\xC0") { + // Valid - continue parsing + } else { + /* char is invalid, we must wait for a resync */ + $invalid = true; + continue 2; + } + } + /* Ok we got a complete char here */ + $this->map['p'][$charPos + $foundChars] = $startOffset + $i + $size; + $i += $j - 1; + ++$foundChars; + } + + return $foundChars; + } +} diff --git a/plugins/email/vendor/symfony/mime/Crypto/DkimOptions.php b/plugins/email/vendor/symfony/mime/Crypto/DkimOptions.php new file mode 100644 index 0000000..171bb25 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Crypto/DkimOptions.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +/** + * A helper providing autocompletion for available DkimSigner options. + * + * @author Fabien Potencier + */ +final class DkimOptions +{ + private $options = []; + + public function toArray(): array + { + return $this->options; + } + + /** + * @return $this + */ + public function algorithm(string $algo): self + { + $this->options['algorithm'] = $algo; + + return $this; + } + + /** + * @return $this + */ + public function signatureExpirationDelay(int $show): self + { + $this->options['signature_expiration_delay'] = $show; + + return $this; + } + + /** + * @return $this + */ + public function bodyMaxLength(int $max): self + { + $this->options['body_max_length'] = $max; + + return $this; + } + + /** + * @return $this + */ + public function bodyShowLength(bool $show): self + { + $this->options['body_show_length'] = $show; + + return $this; + } + + /** + * @return $this + */ + public function headerCanon(string $canon): self + { + $this->options['header_canon'] = $canon; + + return $this; + } + + /** + * @return $this + */ + public function bodyCanon(string $canon): self + { + $this->options['body_canon'] = $canon; + + return $this; + } + + /** + * @return $this + */ + public function headersToIgnore(array $headers): self + { + $this->options['headers_to_ignore'] = $headers; + + return $this; + } +} diff --git a/plugins/email/vendor/symfony/mime/Crypto/DkimSigner.php b/plugins/email/vendor/symfony/mime/Crypto/DkimSigner.php new file mode 100644 index 0000000..f0f7091 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Crypto/DkimSigner.php @@ -0,0 +1,220 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Header\UnstructuredHeader; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\AbstractPart; + +/** + * @author Fabien Potencier + * + * RFC 6376 and 8301 + */ +final class DkimSigner +{ + public const CANON_SIMPLE = 'simple'; + public const CANON_RELAXED = 'relaxed'; + + public const ALGO_SHA256 = 'rsa-sha256'; + public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463 + + private $key; + private $domainName; + private $selector; + private $defaultOptions; + + /** + * @param string $pk The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format) + * @param string $passphrase A passphrase of the private key (if any) + */ + public function __construct(string $pk, string $domainName, string $selector, array $defaultOptions = [], string $passphrase = '') + { + if (!\extension_loaded('openssl')) { + throw new \LogicException('PHP extension "openssl" is required to use DKIM.'); + } + if (!$this->key = openssl_pkey_get_private($pk, $passphrase)) { + throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string()); + } + + $this->domainName = $domainName; + $this->selector = $selector; + $this->defaultOptions = $defaultOptions + [ + 'algorithm' => self::ALGO_SHA256, + 'signature_expiration_delay' => 0, + 'body_max_length' => \PHP_INT_MAX, + 'body_show_length' => false, + 'header_canon' => self::CANON_RELAXED, + 'body_canon' => self::CANON_RELAXED, + 'headers_to_ignore' => [], + ]; + } + + public function sign(Message $message, array $options = []): Message + { + $options += $this->defaultOptions; + if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) { + throw new InvalidArgumentException(sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm'])); + } + $headersToIgnore['return-path'] = true; + $headersToIgnore['x-transport'] = true; + foreach ($options['headers_to_ignore'] as $name) { + $headersToIgnore[strtolower($name)] = true; + } + unset($headersToIgnore['from']); + $signedHeaderNames = []; + $headerCanonData = ''; + $headers = $message->getPreparedHeaders(); + foreach ($headers->getNames() as $name) { + foreach ($headers->all($name) as $header) { + if (isset($headersToIgnore[strtolower($header->getName())])) { + continue; + } + + if ('' !== $header->getBodyAsString()) { + $headerCanonData .= $this->canonicalizeHeader($header->toString(), $options['header_canon']); + $signedHeaderNames[] = $header->getName(); + } + } + } + + [$bodyHash, $bodyLength] = $this->hashBody($message->getBody(), $options['body_canon'], $options['body_max_length']); + + $params = [ + 'v' => '1', + 'q' => 'dns/txt', + 'a' => $options['algorithm'], + 'bh' => base64_encode($bodyHash), + 'd' => $this->domainName, + 'h' => implode(': ', $signedHeaderNames), + 'i' => '@'.$this->domainName, + 's' => $this->selector, + 't' => time(), + 'c' => $options['header_canon'].'/'.$options['body_canon'], + ]; + + if ($options['body_show_length']) { + $params['l'] = $bodyLength; + } + if ($options['signature_expiration_delay']) { + $params['x'] = $params['t'] + $options['signature_expiration_delay']; + } + $value = ''; + foreach ($params as $k => $v) { + $value .= $k.'='.$v.'; '; + } + $value = trim($value); + $header = new UnstructuredHeader('DKIM-Signature', $value); + $headerCanonData .= rtrim($this->canonicalizeHeader($header->toString()."\r\n b=", $options['header_canon'])); + if (self::ALGO_SHA256 === $options['algorithm']) { + if (!openssl_sign($headerCanonData, $signature, $this->key, \OPENSSL_ALGO_SHA256)) { + throw new RuntimeException('Unable to sign DKIM hash: '.openssl_error_string()); + } + } else { + throw new \RuntimeException(sprintf('The "%s" DKIM signing algorithm is not supported yet.', self::ALGO_ED25519)); + } + $header->setValue($value.' b='.trim(chunk_split(base64_encode($signature), 73, ' '))); + $headers->add($header); + + return new Message($headers, $message->getBody()); + } + + private function canonicalizeHeader(string $header, string $headerCanon): string + { + if (self::CANON_RELAXED !== $headerCanon) { + return $header."\r\n"; + } + + $exploded = explode(':', $header, 2); + $name = strtolower(trim($exploded[0])); + $value = str_replace("\r\n", '', $exploded[1]); + $value = trim(preg_replace("/[ \t][ \t]+/", ' ', $value)); + + return $name.':'.$value."\r\n"; + } + + private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength): array + { + $hash = hash_init('sha256'); + $relaxed = self::CANON_RELAXED === $bodyCanon; + $currentLine = ''; + $emptyCounter = 0; + $isSpaceSequence = false; + $length = 0; + foreach ($body->bodyToIterable() as $chunk) { + $canon = ''; + for ($i = 0, $len = \strlen($chunk); $i < $len; ++$i) { + switch ($chunk[$i]) { + case "\r": + break; + case "\n": + // previous char is always \r + if ($relaxed) { + $isSpaceSequence = false; + } + if ('' === $currentLine) { + ++$emptyCounter; + } else { + $currentLine = ''; + $canon .= "\r\n"; + } + break; + case ' ': + case "\t": + if ($relaxed) { + $isSpaceSequence = true; + break; + } + // no break + default: + if ($emptyCounter > 0) { + $canon .= str_repeat("\r\n", $emptyCounter); + $emptyCounter = 0; + } + if ($isSpaceSequence) { + $currentLine .= ' '; + $canon .= ' '; + $isSpaceSequence = false; + } + $currentLine .= $chunk[$i]; + $canon .= $chunk[$i]; + } + } + + if ($length + \strlen($canon) >= $maxLength) { + $canon = substr($canon, 0, $maxLength - $length); + $length += \strlen($canon); + hash_update($hash, $canon); + + break; + } + + $length += \strlen($canon); + hash_update($hash, $canon); + } + + // Add trailing Line return if last line is non empty + if ('' !== $currentLine) { + hash_update($hash, "\r\n"); + $length += \strlen("\r\n"); + } + + if (!$relaxed && 0 === $length) { + hash_update($hash, "\r\n"); + $length = 2; + } + + return [hash_final($hash, true), $length]; + } +} diff --git a/plugins/email/vendor/symfony/mime/Crypto/SMime.php b/plugins/email/vendor/symfony/mime/Crypto/SMime.php new file mode 100644 index 0000000..cba95f2 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Crypto/SMime.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Part\SMimePart; + +/** + * @author Sebastiaan Stok + * + * @internal + */ +abstract class SMime +{ + protected function normalizeFilePath(string $path): string + { + if (!file_exists($path)) { + throw new RuntimeException(sprintf('File does not exist: "%s".', $path)); + } + + return 'file://'.str_replace('\\', '/', realpath($path)); + } + + protected function iteratorToFile(iterable $iterator, $stream): void + { + foreach ($iterator as $chunk) { + fwrite($stream, $chunk); + } + } + + protected function convertMessageToSMimePart($stream, string $type, string $subtype): SMimePart + { + rewind($stream); + + $headers = ''; + + while (!feof($stream)) { + $buffer = fread($stream, 78); + $headers .= $buffer; + + // Detect ending of header list + if (preg_match('/(\r\n\r\n|\n\n)/', $headers, $match)) { + $headersPosEnd = strpos($headers, $headerBodySeparator = $match[0]); + + break; + } + } + + $headers = $this->getMessageHeaders(trim(substr($headers, 0, $headersPosEnd))); + + fseek($stream, $headersPosEnd + \strlen($headerBodySeparator)); + + return new SMimePart($this->getStreamIterator($stream), $type, $subtype, $this->getParametersFromHeader($headers['content-type'])); + } + + protected function getStreamIterator($stream): iterable + { + while (!feof($stream)) { + yield str_replace("\n", "\r\n", str_replace("\r\n", "\n", fread($stream, 16372))); + } + } + + private function getMessageHeaders(string $headerData): array + { + $headers = []; + $headerLines = explode("\r\n", str_replace("\n", "\r\n", str_replace("\r\n", "\n", $headerData))); + $currentHeaderName = ''; + + // Transform header lines into an associative array + foreach ($headerLines as $headerLine) { + // Empty lines between headers indicate a new mime-entity + if ('' === $headerLine) { + break; + } + + // Handle headers that span multiple lines + if (!str_contains($headerLine, ':')) { + $headers[$currentHeaderName] .= ' '.trim($headerLine); + continue; + } + + $header = explode(':', $headerLine, 2); + $currentHeaderName = strtolower($header[0]); + $headers[$currentHeaderName] = trim($header[1]); + } + + return $headers; + } + + private function getParametersFromHeader(string $header): array + { + $params = []; + + preg_match_all('/(?P[a-z-0-9]+)=(?P"[^"]+"|(?:[^\s;]+|$))(?:\s+;)?/i', $header, $matches); + + foreach ($matches['value'] as $pos => $paramValue) { + $params[$matches['name'][$pos]] = trim($paramValue, '"'); + } + + return $params; + } +} diff --git a/plugins/email/vendor/symfony/mime/Crypto/SMimeEncrypter.php b/plugins/email/vendor/symfony/mime/Crypto/SMimeEncrypter.php new file mode 100644 index 0000000..9081860 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Crypto/SMimeEncrypter.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Message; + +/** + * @author Sebastiaan Stok + */ +final class SMimeEncrypter extends SMime +{ + private $certs; + private $cipher; + + /** + * @param string|string[] $certificate The path (or array of paths) of the file(s) containing the X.509 certificate(s) + * @param int|null $cipher A set of algorithms used to encrypt the message. Must be one of these PHP constants: https://www.php.net/manual/en/openssl.ciphers.php + */ + public function __construct($certificate, int $cipher = null) + { + if (!\extension_loaded('openssl')) { + throw new \LogicException('PHP extension "openssl" is required to use SMime.'); + } + + if (\is_array($certificate)) { + $this->certs = array_map([$this, 'normalizeFilePath'], $certificate); + } else { + $this->certs = $this->normalizeFilePath($certificate); + } + + $this->cipher = $cipher ?? \OPENSSL_CIPHER_AES_256_CBC; + } + + public function encrypt(Message $message): Message + { + $bufferFile = tmpfile(); + $outputFile = tmpfile(); + + $this->iteratorToFile($message->toIterable(), $bufferFile); + + if (!@openssl_pkcs7_encrypt(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->certs, [], 0, $this->cipher)) { + throw new RuntimeException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string())); + } + + $mimePart = $this->convertMessageToSMimePart($outputFile, 'application', 'pkcs7-mime'); + $mimePart->getHeaders() + ->addTextHeader('Content-Transfer-Encoding', 'base64') + ->addParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'smime.p7m']) + ; + + return new Message($message->getHeaders(), $mimePart); + } +} diff --git a/plugins/email/vendor/symfony/mime/Crypto/SMimeSigner.php b/plugins/email/vendor/symfony/mime/Crypto/SMimeSigner.php new file mode 100644 index 0000000..5b94a45 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Crypto/SMimeSigner.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Message; + +/** + * @author Sebastiaan Stok + */ +final class SMimeSigner extends SMime +{ + private $signCertificate; + private $signPrivateKey; + private $signOptions; + private $extraCerts; + + /** + * @param string $certificate The path of the file containing the signing certificate (in PEM format) + * @param string $privateKey The path of the file containing the private key (in PEM format) + * @param string|null $privateKeyPassphrase A passphrase of the private key (if any) + * @param string|null $extraCerts The path of the file containing intermediate certificates (in PEM format) needed by the signing certificate + * @param int|null $signOptions Bitwise operator options for openssl_pkcs7_sign() (@see https://secure.php.net/manual/en/openssl.pkcs7.flags.php) + */ + public function __construct(string $certificate, string $privateKey, string $privateKeyPassphrase = null, string $extraCerts = null, int $signOptions = null) + { + if (!\extension_loaded('openssl')) { + throw new \LogicException('PHP extension "openssl" is required to use SMime.'); + } + + $this->signCertificate = $this->normalizeFilePath($certificate); + + if (null !== $privateKeyPassphrase) { + $this->signPrivateKey = [$this->normalizeFilePath($privateKey), $privateKeyPassphrase]; + } else { + $this->signPrivateKey = $this->normalizeFilePath($privateKey); + } + + $this->signOptions = $signOptions ?? \PKCS7_DETACHED; + $this->extraCerts = $extraCerts ? realpath($extraCerts) : null; + } + + public function sign(Message $message): Message + { + $bufferFile = tmpfile(); + $outputFile = tmpfile(); + + $this->iteratorToFile($message->getBody()->toIterable(), $bufferFile); + + if (!@openssl_pkcs7_sign(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->signCertificate, $this->signPrivateKey, [], $this->signOptions, $this->extraCerts)) { + throw new RuntimeException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string())); + } + + return new Message($message->getHeaders(), $this->convertMessageToSMimePart($outputFile, 'multipart', 'signed')); + } +} diff --git a/plugins/email/vendor/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php b/plugins/email/vendor/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php new file mode 100644 index 0000000..00eef94 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Registers custom mime types guessers. + * + * @author Fabien Potencier + */ +class AddMimeTypeGuesserPass implements CompilerPassInterface +{ + private $mimeTypesService; + private $mimeTypeGuesserTag; + + public function __construct(string $mimeTypesService = 'mime_types', string $mimeTypeGuesserTag = 'mime.mime_type_guesser') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/mime', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->mimeTypesService = $mimeTypesService; + $this->mimeTypeGuesserTag = $mimeTypeGuesserTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->has($this->mimeTypesService)) { + $definition = $container->findDefinition($this->mimeTypesService); + foreach ($container->findTaggedServiceIds($this->mimeTypeGuesserTag, true) as $id => $attributes) { + $definition->addMethodCall('registerGuesser', [new Reference($id)]); + } + } + } +} diff --git a/plugins/email/vendor/symfony/mime/Email.php b/plugins/email/vendor/symfony/mime/Email.php new file mode 100644 index 0000000..bd0a476 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Email.php @@ -0,0 +1,630 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\Multipart\AlternativePart; +use Symfony\Component\Mime\Part\Multipart\MixedPart; +use Symfony\Component\Mime\Part\Multipart\RelatedPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @author Fabien Potencier + */ +class Email extends Message +{ + public const PRIORITY_HIGHEST = 1; + public const PRIORITY_HIGH = 2; + public const PRIORITY_NORMAL = 3; + public const PRIORITY_LOW = 4; + public const PRIORITY_LOWEST = 5; + + private const PRIORITY_MAP = [ + self::PRIORITY_HIGHEST => 'Highest', + self::PRIORITY_HIGH => 'High', + self::PRIORITY_NORMAL => 'Normal', + self::PRIORITY_LOW => 'Low', + self::PRIORITY_LOWEST => 'Lowest', + ]; + + private $text; + private $textCharset; + private $html; + private $htmlCharset; + private $attachments = []; + /** + * @var AbstractPart|null + */ + private $cachedBody; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries. + + /** + * @return $this + */ + public function subject(string $subject) + { + return $this->setHeaderBody('Text', 'Subject', $subject); + } + + public function getSubject(): ?string + { + return $this->getHeaders()->getHeaderBody('Subject'); + } + + /** + * @return $this + */ + public function date(\DateTimeInterface $dateTime) + { + return $this->setHeaderBody('Date', 'Date', $dateTime); + } + + public function getDate(): ?\DateTimeImmutable + { + return $this->getHeaders()->getHeaderBody('Date'); + } + + /** + * @param Address|string $address + * + * @return $this + */ + public function returnPath($address) + { + return $this->setHeaderBody('Path', 'Return-Path', Address::create($address)); + } + + public function getReturnPath(): ?Address + { + return $this->getHeaders()->getHeaderBody('Return-Path'); + } + + /** + * @param Address|string $address + * + * @return $this + */ + public function sender($address) + { + return $this->setHeaderBody('Mailbox', 'Sender', Address::create($address)); + } + + public function getSender(): ?Address + { + return $this->getHeaders()->getHeaderBody('Sender'); + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function addFrom(...$addresses) + { + return $this->addListAddressHeaderBody('From', $addresses); + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function from(...$addresses) + { + return $this->setListAddressHeaderBody('From', $addresses); + } + + /** + * @return Address[] + */ + public function getFrom(): array + { + return $this->getHeaders()->getHeaderBody('From') ?: []; + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function addReplyTo(...$addresses) + { + return $this->addListAddressHeaderBody('Reply-To', $addresses); + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function replyTo(...$addresses) + { + return $this->setListAddressHeaderBody('Reply-To', $addresses); + } + + /** + * @return Address[] + */ + public function getReplyTo(): array + { + return $this->getHeaders()->getHeaderBody('Reply-To') ?: []; + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function addTo(...$addresses) + { + return $this->addListAddressHeaderBody('To', $addresses); + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function to(...$addresses) + { + return $this->setListAddressHeaderBody('To', $addresses); + } + + /** + * @return Address[] + */ + public function getTo(): array + { + return $this->getHeaders()->getHeaderBody('To') ?: []; + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function addCc(...$addresses) + { + return $this->addListAddressHeaderBody('Cc', $addresses); + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function cc(...$addresses) + { + return $this->setListAddressHeaderBody('Cc', $addresses); + } + + /** + * @return Address[] + */ + public function getCc(): array + { + return $this->getHeaders()->getHeaderBody('Cc') ?: []; + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function addBcc(...$addresses) + { + return $this->addListAddressHeaderBody('Bcc', $addresses); + } + + /** + * @param Address|string ...$addresses + * + * @return $this + */ + public function bcc(...$addresses) + { + return $this->setListAddressHeaderBody('Bcc', $addresses); + } + + /** + * @return Address[] + */ + public function getBcc(): array + { + return $this->getHeaders()->getHeaderBody('Bcc') ?: []; + } + + /** + * Sets the priority of this message. + * + * The value is an integer where 1 is the highest priority and 5 is the lowest. + * + * @return $this + */ + public function priority(int $priority) + { + if ($priority > 5) { + $priority = 5; + } elseif ($priority < 1) { + $priority = 1; + } + + return $this->setHeaderBody('Text', 'X-Priority', sprintf('%d (%s)', $priority, self::PRIORITY_MAP[$priority])); + } + + /** + * Get the priority of this message. + * + * The returned value is an integer where 1 is the highest priority and 5 + * is the lowest. + */ + public function getPriority(): int + { + [$priority] = sscanf($this->getHeaders()->getHeaderBody('X-Priority') ?? '', '%[1-5]'); + + return $priority ?? 3; + } + + /** + * @param resource|string|null $body + * + * @return $this + */ + public function text($body, string $charset = 'utf-8') + { + if (null !== $body && !\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); + } + + $this->cachedBody = null; + $this->text = $body; + $this->textCharset = $charset; + + return $this; + } + + /** + * @return resource|string|null + */ + public function getTextBody() + { + return $this->text; + } + + public function getTextCharset(): ?string + { + return $this->textCharset; + } + + /** + * @param resource|string|null $body + * + * @return $this + */ + public function html($body, string $charset = 'utf-8') + { + if (null !== $body && !\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); + } + + $this->cachedBody = null; + $this->html = $body; + $this->htmlCharset = $charset; + + return $this; + } + + /** + * @return resource|string|null + */ + public function getHtmlBody() + { + return $this->html; + } + + public function getHtmlCharset(): ?string + { + return $this->htmlCharset; + } + + /** + * @param resource|string $body + * + * @return $this + */ + public function attach($body, string $name = null, string $contentType = null) + { + if (!\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body))); + } + + $this->cachedBody = null; + $this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => false]; + + return $this; + } + + /** + * @return $this + */ + public function attachFromPath(string $path, string $name = null, string $contentType = null) + { + $this->cachedBody = null; + $this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => false]; + + return $this; + } + + /** + * @param resource|string $body + * + * @return $this + */ + public function embed($body, string $name = null, string $contentType = null) + { + if (!\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body))); + } + + $this->cachedBody = null; + $this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => true]; + + return $this; + } + + /** + * @return $this + */ + public function embedFromPath(string $path, string $name = null, string $contentType = null) + { + $this->cachedBody = null; + $this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => true]; + + return $this; + } + + /** + * @return $this + */ + public function attachPart(DataPart $part) + { + $this->cachedBody = null; + $this->attachments[] = ['part' => $part]; + + return $this; + } + + /** + * @return array|DataPart[] + */ + public function getAttachments(): array + { + $parts = []; + foreach ($this->attachments as $attachment) { + $parts[] = $this->createDataPart($attachment); + } + + return $parts; + } + + public function getBody(): AbstractPart + { + if (null !== $body = parent::getBody()) { + return $body; + } + + return $this->generateBody(); + } + + public function ensureValidity() + { + if (null === $this->text && null === $this->html && !$this->attachments) { + throw new LogicException('A message must have a text or an HTML part or attachments.'); + } + + parent::ensureValidity(); + } + + /** + * Generates an AbstractPart based on the raw body of a message. + * + * The most "complex" part generated by this method is when there is text and HTML bodies + * with related images for the HTML part and some attachments: + * + * multipart/mixed + * | + * |------------> multipart/related + * | | + * | |------------> multipart/alternative + * | | | + * | | ------------> text/plain (with content) + * | | | + * | | ------------> text/html (with content) + * | | + * | ------------> image/png (with content) + * | + * ------------> application/pdf (with content) + */ + private function generateBody(): AbstractPart + { + if (null !== $this->cachedBody) { + return $this->cachedBody; + } + + $this->ensureValidity(); + + [$htmlPart, $otherParts, $relatedParts] = $this->prepareParts(); + + $part = null === $this->text ? null : new TextPart($this->text, $this->textCharset); + if (null !== $htmlPart) { + if (null !== $part) { + $part = new AlternativePart($part, $htmlPart); + } else { + $part = $htmlPart; + } + } + + if ($relatedParts) { + $part = new RelatedPart($part, ...$relatedParts); + } + + if ($otherParts) { + if ($part) { + $part = new MixedPart($part, ...$otherParts); + } else { + $part = new MixedPart(...$otherParts); + } + } + + return $this->cachedBody = $part; + } + + private function prepareParts(): ?array + { + $names = []; + $htmlPart = null; + $html = $this->html; + if (null !== $html) { + $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); + $html = $htmlPart->getBody(); + preg_match_all('(]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+)))i', $html, $names); + $names = array_filter(array_unique(array_merge($names[2], $names[3]))); + } + + // usage of reflection is a temporary workaround for missing getters that will be added in 6.2 + $nameRef = new \ReflectionProperty(TextPart::class, 'name'); + $nameRef->setAccessible(true); + $otherParts = $relatedParts = []; + foreach ($this->attachments as $attachment) { + $part = $this->createDataPart($attachment); + if (isset($attachment['part'])) { + $attachment['name'] = $nameRef->getValue($part); + } + + $related = false; + foreach ($names as $name) { + if ($name !== $attachment['name']) { + continue; + } + if (isset($relatedParts[$name])) { + continue 2; + } + $part->setDisposition('inline'); + $html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html, $count); + if ($count) { + $related = true; + } + $part->setName($part->getContentId()); + + break; + } + + if ($related) { + $relatedParts[$attachment['name']] = $part; + } else { + $otherParts[] = $part; + } + } + if (null !== $htmlPart) { + $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); + } + + return [$htmlPart, $otherParts, array_values($relatedParts)]; + } + + private function createDataPart(array $attachment): DataPart + { + if (isset($attachment['part'])) { + return $attachment['part']; + } + + if (isset($attachment['body'])) { + $part = new DataPart($attachment['body'], $attachment['name'] ?? null, $attachment['content-type'] ?? null); + } else { + $part = DataPart::fromPath($attachment['path'] ?? '', $attachment['name'] ?? null, $attachment['content-type'] ?? null); + } + if ($attachment['inline']) { + $part->asInline(); + } + + return $part; + } + + /** + * @return $this + */ + private function setHeaderBody(string $type, string $name, $body): object + { + $this->getHeaders()->setHeaderBody($type, $name, $body); + + return $this; + } + + private function addListAddressHeaderBody(string $name, array $addresses) + { + if (!$header = $this->getHeaders()->get($name)) { + return $this->setListAddressHeaderBody($name, $addresses); + } + $header->addAddresses(Address::createArray($addresses)); + + return $this; + } + + /** + * @return $this + */ + private function setListAddressHeaderBody(string $name, array $addresses) + { + $addresses = Address::createArray($addresses); + $headers = $this->getHeaders(); + if ($header = $headers->get($name)) { + $header->setAddresses($addresses); + } else { + $headers->addMailboxListHeader($name, $addresses); + } + + return $this; + } + + /** + * @internal + */ + public function __serialize(): array + { + if (\is_resource($this->text)) { + $this->text = (new TextPart($this->text))->getBody(); + } + + if (\is_resource($this->html)) { + $this->html = (new TextPart($this->html))->getBody(); + } + + foreach ($this->attachments as $i => $attachment) { + if (isset($attachment['body']) && \is_resource($attachment['body'])) { + $this->attachments[$i]['body'] = (new TextPart($attachment['body']))->getBody(); + } + } + + return [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, parent::__serialize()]; + } + + /** + * @internal + */ + public function __unserialize(array $data): void + { + [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, $parentData] = $data; + + parent::__unserialize($parentData); + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/AddressEncoderInterface.php b/plugins/email/vendor/symfony/mime/Encoder/AddressEncoderInterface.php new file mode 100644 index 0000000..de477d8 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/AddressEncoderInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\Exception\AddressEncoderException; + +/** + * @author Christian Schmidt + */ +interface AddressEncoderInterface +{ + /** + * Encodes an email address. + * + * @throws AddressEncoderException if the email cannot be represented in + * the encoding implemented by this class + */ + public function encodeString(string $address): string; +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/Base64ContentEncoder.php b/plugins/email/vendor/symfony/mime/Encoder/Base64ContentEncoder.php new file mode 100644 index 0000000..440868a --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/Base64ContentEncoder.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\Exception\RuntimeException; + +/** + * @author Fabien Potencier + */ +final class Base64ContentEncoder extends Base64Encoder implements ContentEncoderInterface +{ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + if (!\is_resource($stream)) { + throw new \TypeError(sprintf('Method "%s" takes a stream as a first argument.', __METHOD__)); + } + + $filter = stream_filter_append($stream, 'convert.base64-encode', \STREAM_FILTER_READ, [ + 'line-length' => 0 >= $maxLineLength || 76 < $maxLineLength ? 76 : $maxLineLength, + 'line-break-chars' => "\r\n", + ]); + if (!\is_resource($filter)) { + throw new RuntimeException('Unable to set the base64 content encoder to the filter.'); + } + + while (!feof($stream)) { + yield fread($stream, 16372); + } + stream_filter_remove($filter); + } + + public function getName(): string + { + return 'base64'; + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/Base64Encoder.php b/plugins/email/vendor/symfony/mime/Encoder/Base64Encoder.php new file mode 100644 index 0000000..7106478 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/Base64Encoder.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +class Base64Encoder implements EncoderInterface +{ + /** + * Takes an unencoded string and produces a Base64 encoded string from it. + * + * Base64 encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + if (0 >= $maxLineLength || 76 < $maxLineLength) { + $maxLineLength = 76; + } + + $encodedString = base64_encode($string); + $firstLine = ''; + if (0 !== $firstLineOffset) { + $firstLine = substr($encodedString, 0, $maxLineLength - $firstLineOffset)."\r\n"; + $encodedString = substr($encodedString, $maxLineLength - $firstLineOffset); + } + + return $firstLine.trim(chunk_split($encodedString, $maxLineLength, "\r\n")); + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/Base64MimeHeaderEncoder.php b/plugins/email/vendor/symfony/mime/Encoder/Base64MimeHeaderEncoder.php new file mode 100644 index 0000000..5c06f6d --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/Base64MimeHeaderEncoder.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +final class Base64MimeHeaderEncoder extends Base64Encoder implements MimeHeaderEncoderInterface +{ + public function getName(): string + { + return 'B'; + } + + /** + * Takes an unencoded string and produces a Base64 encoded string from it. + * + * If the charset is iso-2022-jp, it uses mb_encode_mimeheader instead of + * default encodeString, otherwise pass to the parent method. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + if ('iso-2022-jp' === strtolower($charset)) { + $old = mb_internal_encoding(); + mb_internal_encoding('utf-8'); + $newstring = mb_encode_mimeheader($string, 'iso-2022-jp', $this->getName(), "\r\n"); + mb_internal_encoding($old); + + return $newstring; + } + + return parent::encodeString($string, $charset, $firstLineOffset, $maxLineLength); + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/ContentEncoderInterface.php b/plugins/email/vendor/symfony/mime/Encoder/ContentEncoderInterface.php new file mode 100644 index 0000000..a45ad04 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/ContentEncoderInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +interface ContentEncoderInterface extends EncoderInterface +{ + /** + * Encodes the stream to a Generator. + * + * @param resource $stream + */ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable; + + /** + * Gets the MIME name of this content encoding scheme. + */ + public function getName(): string; +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/EightBitContentEncoder.php b/plugins/email/vendor/symfony/mime/Encoder/EightBitContentEncoder.php new file mode 100644 index 0000000..8283120 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/EightBitContentEncoder.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Fabien Potencier + */ +final class EightBitContentEncoder implements ContentEncoderInterface +{ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + while (!feof($stream)) { + yield fread($stream, 16372); + } + } + + public function getName(): string + { + return '8bit'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return $string; + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/EncoderInterface.php b/plugins/email/vendor/symfony/mime/Encoder/EncoderInterface.php new file mode 100644 index 0000000..bbf6d48 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/EncoderInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +interface EncoderInterface +{ + /** + * Encode a given string to produce an encoded string. + * + * @param int $firstLineOffset if first line needs to be shorter + * @param int $maxLineLength - 0 indicates the default length for this encoding + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string; +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/IdnAddressEncoder.php b/plugins/email/vendor/symfony/mime/Encoder/IdnAddressEncoder.php new file mode 100644 index 0000000..b56e7e3 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/IdnAddressEncoder.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * An IDN email address encoder. + * + * Encodes the domain part of an address using IDN. This is compatible will all + * SMTP servers. + * + * Note: It leaves the local part as is. In case there are non-ASCII characters + * in the local part then it depends on the SMTP Server if this is supported. + * + * @author Christian Schmidt + */ +final class IdnAddressEncoder implements AddressEncoderInterface +{ + /** + * Encodes the domain part of an address using IDN. + */ + public function encodeString(string $address): string + { + $i = strrpos($address, '@'); + if (false !== $i) { + $local = substr($address, 0, $i); + $domain = substr($address, $i + 1); + + if (preg_match('/[^\x00-\x7F]/', $domain)) { + $address = sprintf('%s@%s', $local, idn_to_ascii($domain, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46)); + } + } + + return $address; + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/MimeHeaderEncoderInterface.php b/plugins/email/vendor/symfony/mime/Encoder/MimeHeaderEncoderInterface.php new file mode 100644 index 0000000..fff2c78 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/MimeHeaderEncoderInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +interface MimeHeaderEncoderInterface +{ + /** + * Get the MIME name of this content encoding scheme. + */ + public function getName(): string; +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/QpContentEncoder.php b/plugins/email/vendor/symfony/mime/Encoder/QpContentEncoder.php new file mode 100644 index 0000000..4703cc2 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/QpContentEncoder.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Lars Strojny + */ +final class QpContentEncoder implements ContentEncoderInterface +{ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + if (!\is_resource($stream)) { + throw new \TypeError(sprintf('Method "%s" takes a stream as a first argument.', __METHOD__)); + } + + // we don't use PHP stream filters here as the content should be small enough + yield $this->encodeString(stream_get_contents($stream), 'utf-8', 0, $maxLineLength); + } + + public function getName(): string + { + return 'quoted-printable'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return $this->standardize(quoted_printable_encode($string)); + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + */ + private function standardize(string $string): string + { + // transform CR or LF to CRLF + $string = preg_replace('~=0D(?!=0A)|(? + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\CharacterStream; + +/** + * @author Chris Corbyn + */ +class QpEncoder implements EncoderInterface +{ + /** + * Pre-computed QP for HUGE optimization. + */ + private const QP_MAP = [ + 0 => '=00', 1 => '=01', 2 => '=02', 3 => '=03', 4 => '=04', + 5 => '=05', 6 => '=06', 7 => '=07', 8 => '=08', 9 => '=09', + 10 => '=0A', 11 => '=0B', 12 => '=0C', 13 => '=0D', 14 => '=0E', + 15 => '=0F', 16 => '=10', 17 => '=11', 18 => '=12', 19 => '=13', + 20 => '=14', 21 => '=15', 22 => '=16', 23 => '=17', 24 => '=18', + 25 => '=19', 26 => '=1A', 27 => '=1B', 28 => '=1C', 29 => '=1D', + 30 => '=1E', 31 => '=1F', 32 => '=20', 33 => '=21', 34 => '=22', + 35 => '=23', 36 => '=24', 37 => '=25', 38 => '=26', 39 => '=27', + 40 => '=28', 41 => '=29', 42 => '=2A', 43 => '=2B', 44 => '=2C', + 45 => '=2D', 46 => '=2E', 47 => '=2F', 48 => '=30', 49 => '=31', + 50 => '=32', 51 => '=33', 52 => '=34', 53 => '=35', 54 => '=36', + 55 => '=37', 56 => '=38', 57 => '=39', 58 => '=3A', 59 => '=3B', + 60 => '=3C', 61 => '=3D', 62 => '=3E', 63 => '=3F', 64 => '=40', + 65 => '=41', 66 => '=42', 67 => '=43', 68 => '=44', 69 => '=45', + 70 => '=46', 71 => '=47', 72 => '=48', 73 => '=49', 74 => '=4A', + 75 => '=4B', 76 => '=4C', 77 => '=4D', 78 => '=4E', 79 => '=4F', + 80 => '=50', 81 => '=51', 82 => '=52', 83 => '=53', 84 => '=54', + 85 => '=55', 86 => '=56', 87 => '=57', 88 => '=58', 89 => '=59', + 90 => '=5A', 91 => '=5B', 92 => '=5C', 93 => '=5D', 94 => '=5E', + 95 => '=5F', 96 => '=60', 97 => '=61', 98 => '=62', 99 => '=63', + 100 => '=64', 101 => '=65', 102 => '=66', 103 => '=67', 104 => '=68', + 105 => '=69', 106 => '=6A', 107 => '=6B', 108 => '=6C', 109 => '=6D', + 110 => '=6E', 111 => '=6F', 112 => '=70', 113 => '=71', 114 => '=72', + 115 => '=73', 116 => '=74', 117 => '=75', 118 => '=76', 119 => '=77', + 120 => '=78', 121 => '=79', 122 => '=7A', 123 => '=7B', 124 => '=7C', + 125 => '=7D', 126 => '=7E', 127 => '=7F', 128 => '=80', 129 => '=81', + 130 => '=82', 131 => '=83', 132 => '=84', 133 => '=85', 134 => '=86', + 135 => '=87', 136 => '=88', 137 => '=89', 138 => '=8A', 139 => '=8B', + 140 => '=8C', 141 => '=8D', 142 => '=8E', 143 => '=8F', 144 => '=90', + 145 => '=91', 146 => '=92', 147 => '=93', 148 => '=94', 149 => '=95', + 150 => '=96', 151 => '=97', 152 => '=98', 153 => '=99', 154 => '=9A', + 155 => '=9B', 156 => '=9C', 157 => '=9D', 158 => '=9E', 159 => '=9F', + 160 => '=A0', 161 => '=A1', 162 => '=A2', 163 => '=A3', 164 => '=A4', + 165 => '=A5', 166 => '=A6', 167 => '=A7', 168 => '=A8', 169 => '=A9', + 170 => '=AA', 171 => '=AB', 172 => '=AC', 173 => '=AD', 174 => '=AE', + 175 => '=AF', 176 => '=B0', 177 => '=B1', 178 => '=B2', 179 => '=B3', + 180 => '=B4', 181 => '=B5', 182 => '=B6', 183 => '=B7', 184 => '=B8', + 185 => '=B9', 186 => '=BA', 187 => '=BB', 188 => '=BC', 189 => '=BD', + 190 => '=BE', 191 => '=BF', 192 => '=C0', 193 => '=C1', 194 => '=C2', + 195 => '=C3', 196 => '=C4', 197 => '=C5', 198 => '=C6', 199 => '=C7', + 200 => '=C8', 201 => '=C9', 202 => '=CA', 203 => '=CB', 204 => '=CC', + 205 => '=CD', 206 => '=CE', 207 => '=CF', 208 => '=D0', 209 => '=D1', + 210 => '=D2', 211 => '=D3', 212 => '=D4', 213 => '=D5', 214 => '=D6', + 215 => '=D7', 216 => '=D8', 217 => '=D9', 218 => '=DA', 219 => '=DB', + 220 => '=DC', 221 => '=DD', 222 => '=DE', 223 => '=DF', 224 => '=E0', + 225 => '=E1', 226 => '=E2', 227 => '=E3', 228 => '=E4', 229 => '=E5', + 230 => '=E6', 231 => '=E7', 232 => '=E8', 233 => '=E9', 234 => '=EA', + 235 => '=EB', 236 => '=EC', 237 => '=ED', 238 => '=EE', 239 => '=EF', + 240 => '=F0', 241 => '=F1', 242 => '=F2', 243 => '=F3', 244 => '=F4', + 245 => '=F5', 246 => '=F6', 247 => '=F7', 248 => '=F8', 249 => '=F9', + 250 => '=FA', 251 => '=FB', 252 => '=FC', 253 => '=FD', 254 => '=FE', + 255 => '=FF', + ]; + + private static $safeMapShare = []; + + /** + * A map of non-encoded ascii characters. + * + * @var string[] + * + * @internal + */ + protected $safeMap = []; + + public function __construct() + { + $id = static::class; + if (!isset(self::$safeMapShare[$id])) { + $this->initSafeMap(); + self::$safeMapShare[$id] = $this->safeMap; + } else { + $this->safeMap = self::$safeMapShare[$id]; + } + } + + protected function initSafeMap(): void + { + foreach (array_merge([0x09, 0x20], range(0x21, 0x3C), range(0x3E, 0x7E)) as $byte) { + $this->safeMap[$byte] = \chr($byte); + } + } + + /** + * {@inheritdoc} + * + * Takes an unencoded string and produces a QP encoded string from it. + * + * QP encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + if ($maxLineLength > 76 || $maxLineLength <= 0) { + $maxLineLength = 76; + } + + $thisLineLength = $maxLineLength - $firstLineOffset; + + $lines = []; + $lNo = 0; + $lines[$lNo] = ''; + $currentLine = &$lines[$lNo++]; + $size = $lineLen = 0; + $charStream = new CharacterStream($string, $charset); + + // Fetching more than 4 chars at one is slower, as is fetching fewer bytes + // Conveniently 4 chars is the UTF-8 safe number since UTF-8 has up to 6 + // bytes per char and (6 * 4 * 3 = 72 chars per line) * =NN is 3 bytes + while (null !== $bytes = $charStream->readBytes(4)) { + $enc = $this->encodeByteSequence($bytes, $size); + + $i = strpos($enc, '=0D=0A'); + $newLineLength = $lineLen + (false === $i ? $size : $i); + + if ($currentLine && $newLineLength >= $thisLineLength) { + $lines[$lNo] = ''; + $currentLine = &$lines[$lNo++]; + $thisLineLength = $maxLineLength; + $lineLen = 0; + } + + $currentLine .= $enc; + + if (false === $i) { + $lineLen += $size; + } else { + // 6 is the length of '=0D=0A'. + $lineLen = $size - strrpos($enc, '=0D=0A') - 6; + } + } + + return $this->standardize(implode("=\r\n", $lines)); + } + + /** + * Encode the given byte array into a verbatim QP form. + */ + private function encodeByteSequence(array $bytes, int &$size): string + { + $ret = ''; + $size = 0; + foreach ($bytes as $b) { + if (isset($this->safeMap[$b])) { + $ret .= $this->safeMap[$b]; + ++$size; + } else { + $ret .= self::QP_MAP[$b]; + $size += 3; + } + } + + return $ret; + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + */ + private function standardize(string $string): string + { + $string = str_replace(["\t=0D=0A", ' =0D=0A', '=0D=0A'], ["=09\r\n", "=20\r\n", "\r\n"], $string); + switch ($end = \ord(substr($string, -1))) { + case 0x09: + case 0x20: + $string = substr_replace($string, self::QP_MAP[$end], -1); + } + + return $string; + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/QpMimeHeaderEncoder.php b/plugins/email/vendor/symfony/mime/Encoder/QpMimeHeaderEncoder.php new file mode 100644 index 0000000..d1d3837 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/QpMimeHeaderEncoder.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +final class QpMimeHeaderEncoder extends QpEncoder implements MimeHeaderEncoderInterface +{ + protected function initSafeMap(): void + { + foreach (array_merge( + range(0x61, 0x7A), range(0x41, 0x5A), + range(0x30, 0x39), [0x20, 0x21, 0x2A, 0x2B, 0x2D, 0x2F] + ) as $byte) { + $this->safeMap[$byte] = \chr($byte); + } + } + + public function getName(): string + { + return 'Q'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return str_replace([' ', '=20', "=\r\n"], ['_', '_', "\r\n"], + parent::encodeString($string, $charset, $firstLineOffset, $maxLineLength) + ); + } +} diff --git a/plugins/email/vendor/symfony/mime/Encoder/Rfc2231Encoder.php b/plugins/email/vendor/symfony/mime/Encoder/Rfc2231Encoder.php new file mode 100644 index 0000000..4d93dc6 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Encoder/Rfc2231Encoder.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\CharacterStream; + +/** + * @author Chris Corbyn + */ +final class Rfc2231Encoder implements EncoderInterface +{ + /** + * Takes an unencoded string and produces a string encoded according to RFC 2231 from it. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + $lines = []; + $lineCount = 0; + $lines[] = ''; + $currentLine = &$lines[$lineCount++]; + + if (0 >= $maxLineLength) { + $maxLineLength = 75; + } + + $charStream = new CharacterStream($string, $charset); + $thisLineLength = $maxLineLength - $firstLineOffset; + + while (null !== $char = $charStream->read(4)) { + $encodedChar = rawurlencode($char); + if ('' !== $currentLine && \strlen($currentLine.$encodedChar) > $thisLineLength) { + $lines[] = ''; + $currentLine = &$lines[$lineCount++]; + $thisLineLength = $maxLineLength; + } + $currentLine .= $encodedChar; + } + + return implode("\r\n", $lines); + } +} diff --git a/plugins/email/vendor/symfony/mime/Exception/AddressEncoderException.php b/plugins/email/vendor/symfony/mime/Exception/AddressEncoderException.php new file mode 100644 index 0000000..51ee2e0 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Exception/AddressEncoderException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class AddressEncoderException extends RfcComplianceException +{ +} diff --git a/plugins/email/vendor/symfony/mime/Exception/ExceptionInterface.php b/plugins/email/vendor/symfony/mime/Exception/ExceptionInterface.php new file mode 100644 index 0000000..1193390 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/plugins/email/vendor/symfony/mime/Exception/InvalidArgumentException.php b/plugins/email/vendor/symfony/mime/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..e89ebae --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mime/Exception/LogicException.php b/plugins/email/vendor/symfony/mime/Exception/LogicException.php new file mode 100644 index 0000000..0508ee7 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mime/Exception/RfcComplianceException.php b/plugins/email/vendor/symfony/mime/Exception/RfcComplianceException.php new file mode 100644 index 0000000..26e4a50 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Exception/RfcComplianceException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class RfcComplianceException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mime/Exception/RuntimeException.php b/plugins/email/vendor/symfony/mime/Exception/RuntimeException.php new file mode 100644 index 0000000..fb018b0 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/plugins/email/vendor/symfony/mime/FileBinaryMimeTypeGuesser.php b/plugins/email/vendor/symfony/mime/FileBinaryMimeTypeGuesser.php new file mode 100644 index 0000000..3acf4d4 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/FileBinaryMimeTypeGuesser.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\LogicException; + +/** + * Guesses the MIME type with the binary "file" (only available on *nix). + * + * @author Bernhard Schussek + */ +class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface +{ + private $cmd; + + /** + * The $cmd pattern must contain a "%s" string that will be replaced + * with the file name to guess. + * + * The command output must start with the MIME type of the file. + * + * @param string $cmd The command to run to get the MIME type of a file + */ + public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null') + { + $this->cmd = $cmd; + } + + /** + * {@inheritdoc} + */ + public function isGuesserSupported(): bool + { + static $supported = null; + + if (null !== $supported) { + return $supported; + } + + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) { + return $supported = false; + } + + ob_start(); + passthru('command -v file', $exitStatus); + $binPath = trim(ob_get_clean()); + + return $supported = 0 === $exitStatus && '' !== $binPath; + } + + /** + * {@inheritdoc} + */ + public function guessMimeType(string $path): ?string + { + if (!is_file($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path)); + } + + if (!$this->isGuesserSupported()) { + throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__)); + } + + ob_start(); + + // need to use --mime instead of -i. see #6641 + passthru(sprintf($this->cmd, escapeshellarg((str_starts_with($path, '-') ? './' : '').$path)), $return); + if ($return > 0) { + ob_end_clean(); + + return null; + } + + $type = trim(ob_get_clean()); + + if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\+\.]+)#i', $type, $match)) { + // it's not a type, but an error message + return null; + } + + return $match[1]; + } +} diff --git a/plugins/email/vendor/symfony/mime/FileinfoMimeTypeGuesser.php b/plugins/email/vendor/symfony/mime/FileinfoMimeTypeGuesser.php new file mode 100644 index 0000000..c6c7559 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/FileinfoMimeTypeGuesser.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\LogicException; + +/** + * Guesses the MIME type using the PECL extension FileInfo. + * + * @author Bernhard Schussek + */ +class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface +{ + private $magicFile; + + /** + * @param string $magicFile A magic file to use with the finfo instance + * + * @see http://www.php.net/manual/en/function.finfo-open.php + */ + public function __construct(string $magicFile = null) + { + $this->magicFile = $magicFile; + } + + /** + * {@inheritdoc} + */ + public function isGuesserSupported(): bool + { + return \function_exists('finfo_open'); + } + + /** + * {@inheritdoc} + */ + public function guessMimeType(string $path): ?string + { + if (!is_file($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path)); + } + + if (!$this->isGuesserSupported()) { + throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__)); + } + + if (false === $finfo = new \finfo(\FILEINFO_MIME_TYPE, $this->magicFile)) { + return null; + } + $mimeType = $finfo->file($path); + + if ($mimeType && 0 === (\strlen($mimeType) % 2)) { + $mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1); + $mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType; + } + + return $mimeType; + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/AbstractHeader.php b/plugins/email/vendor/symfony/mime/Header/AbstractHeader.php new file mode 100644 index 0000000..5de9066 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/AbstractHeader.php @@ -0,0 +1,284 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder; + +/** + * An abstract base MIME Header. + * + * @author Chris Corbyn + */ +abstract class AbstractHeader implements HeaderInterface +{ + public const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)'; + + private static $encoder; + + private $name; + private $lineLength = 76; + private $lang; + private $charset = 'utf-8'; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function setCharset(string $charset) + { + $this->charset = $charset; + } + + public function getCharset(): ?string + { + return $this->charset; + } + + /** + * Set the language used in this Header. + * + * For example, for US English, 'en-us'. + */ + public function setLanguage(string $lang) + { + $this->lang = $lang; + } + + public function getLanguage(): ?string + { + return $this->lang; + } + + public function getName(): string + { + return $this->name; + } + + public function setMaxLineLength(int $lineLength) + { + $this->lineLength = $lineLength; + } + + public function getMaxLineLength(): int + { + return $this->lineLength; + } + + public function toString(): string + { + return $this->tokensToString($this->toTokens()); + } + + /** + * Produces a compliant, formatted RFC 2822 'phrase' based on the string given. + * + * @param string $string as displayed + * @param bool $shorten the first line to make remove for header name + */ + protected function createPhrase(HeaderInterface $header, string $string, string $charset, bool $shorten = false): string + { + // Treat token as exactly what was given + $phraseStr = $string; + + // If it's not valid + if (!preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)) { + // .. but it is just ascii text, try escaping some characters + // and make it a quoted-string + if (preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $phraseStr)) { + foreach (['\\', '"'] as $char) { + $phraseStr = str_replace($char, '\\'.$char, $phraseStr); + } + $phraseStr = '"'.$phraseStr.'"'; + } else { + // ... otherwise it needs encoding + // Determine space remaining on line if first line + if ($shorten) { + $usedLength = \strlen($header->getName().': '); + } else { + $usedLength = 0; + } + $phraseStr = $this->encodeWords($header, $string, $usedLength); + } + } elseif (str_contains($phraseStr, '(')) { + foreach (['\\', '"'] as $char) { + $phraseStr = str_replace($char, '\\'.$char, $phraseStr); + } + $phraseStr = '"'.$phraseStr.'"'; + } + + return $phraseStr; + } + + /** + * Encode needed word tokens within a string of input. + */ + protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string + { + $value = ''; + $tokens = $this->getEncodableWordTokens($input); + foreach ($tokens as $token) { + // See RFC 2822, Sect 2.2 (really 2.2 ??) + if ($this->tokenNeedsEncoding($token)) { + // Don't encode starting WSP + $firstChar = substr($token, 0, 1); + switch ($firstChar) { + case ' ': + case "\t": + $value .= $firstChar; + $token = substr($token, 1); + } + + if (-1 == $usedLength) { + $usedLength = \strlen($header->getName().': ') + \strlen($value); + } + $value .= $this->getTokenAsEncodedWord($token, $usedLength); + } else { + $value .= $token; + } + } + + return $value; + } + + protected function tokenNeedsEncoding(string $token): bool + { + return (bool) preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token); + } + + /** + * Splits a string into tokens in blocks of words which can be encoded quickly. + * + * @return string[] + */ + protected function getEncodableWordTokens(string $string): array + { + $tokens = []; + $encodedToken = ''; + // Split at all whitespace boundaries + foreach (preg_split('~(?=[\t ])~', $string) as $token) { + if ($this->tokenNeedsEncoding($token)) { + $encodedToken .= $token; + } else { + if ('' !== $encodedToken) { + $tokens[] = $encodedToken; + $encodedToken = ''; + } + $tokens[] = $token; + } + } + if ('' !== $encodedToken) { + $tokens[] = $encodedToken; + } + + return $tokens; + } + + /** + * Get a token as an encoded word for safe insertion into headers. + */ + protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string + { + if (null === self::$encoder) { + self::$encoder = new QpMimeHeaderEncoder(); + } + + // Adjust $firstLineOffset to account for space needed for syntax + $charsetDecl = $this->charset; + if (null !== $this->lang) { + $charsetDecl .= '*'.$this->lang; + } + $encodingWrapperLength = \strlen('=?'.$charsetDecl.'?'.self::$encoder->getName().'??='); + + if ($firstLineOffset >= 75) { + // Does this logic need to be here? + $firstLineOffset = 0; + } + + $encodedTextLines = explode("\r\n", + self::$encoder->encodeString($token, $this->charset, $firstLineOffset, 75 - $encodingWrapperLength) + ); + + if ('iso-2022-jp' !== strtolower($this->charset)) { + // special encoding for iso-2022-jp using mb_encode_mimeheader + foreach ($encodedTextLines as $lineNum => $line) { + $encodedTextLines[$lineNum] = '=?'.$charsetDecl.'?'.self::$encoder->getName().'?'.$line.'?='; + } + } + + return implode("\r\n ", $encodedTextLines); + } + + /** + * Generates tokens from the given string which include CRLF as individual tokens. + * + * @return string[] + */ + protected function generateTokenLines(string $token): array + { + return preg_split('~(\r\n)~', $token, -1, \PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Generate a list of all tokens in the final header. + */ + protected function toTokens(string $string = null): array + { + if (null === $string) { + $string = $this->getBodyAsString(); + } + + $tokens = []; + // Generate atoms; split at all invisible boundaries followed by WSP + foreach (preg_split('~(?=[ \t])~', $string) as $token) { + $newTokens = $this->generateTokenLines($token); + foreach ($newTokens as $newToken) { + $tokens[] = $newToken; + } + } + + return $tokens; + } + + /** + * Takes an array of tokens which appear in the header and turns them into + * an RFC 2822 compliant string, adding FWSP where needed. + * + * @param string[] $tokens + */ + private function tokensToString(array $tokens): string + { + $lineCount = 0; + $headerLines = []; + $headerLines[] = $this->name.': '; + $currentLine = &$headerLines[$lineCount++]; + + // Build all tokens back into compliant header + foreach ($tokens as $i => $token) { + // Line longer than specified maximum or token was just a new line + if (("\r\n" === $token) || + ($i > 0 && \strlen($currentLine.$token) > $this->lineLength) + && '' !== $currentLine) { + $headerLines[] = ''; + $currentLine = &$headerLines[$lineCount++]; + } + + // Append token to the line + if ("\r\n" !== $token) { + $currentLine .= $token; + } + } + + // Implode with FWS (RFC 2822, 2.2.3) + return implode("\r\n", $headerLines); + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/DateHeader.php b/plugins/email/vendor/symfony/mime/Header/DateHeader.php new file mode 100644 index 0000000..a7385d4 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/DateHeader.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +/** + * A Date MIME Header. + * + * @author Chris Corbyn + */ +final class DateHeader extends AbstractHeader +{ + private $dateTime; + + public function __construct(string $name, \DateTimeInterface $date) + { + parent::__construct($name); + + $this->setDateTime($date); + } + + /** + * @param \DateTimeInterface $body + */ + public function setBody($body) + { + $this->setDateTime($body); + } + + public function getBody(): \DateTimeImmutable + { + return $this->getDateTime(); + } + + public function getDateTime(): \DateTimeImmutable + { + return $this->dateTime; + } + + /** + * Set the date-time of the Date in this Header. + * + * If a DateTime instance is provided, it is converted to DateTimeImmutable. + */ + public function setDateTime(\DateTimeInterface $dateTime) + { + if ($dateTime instanceof \DateTime) { + $immutable = new \DateTimeImmutable('@'.$dateTime->getTimestamp()); + $dateTime = $immutable->setTimezone($dateTime->getTimezone()); + } + $this->dateTime = $dateTime; + } + + public function getBodyAsString(): string + { + return $this->dateTime->format(\DateTime::RFC2822); + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/HeaderInterface.php b/plugins/email/vendor/symfony/mime/Header/HeaderInterface.php new file mode 100644 index 0000000..4546947 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/HeaderInterface.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +/** + * A MIME Header. + * + * @author Chris Corbyn + */ +interface HeaderInterface +{ + /** + * Sets the body. + * + * The type depends on the Header concrete class. + * + * @param mixed $body + */ + public function setBody($body); + + /** + * Gets the body. + * + * The return type depends on the Header concrete class. + * + * @return mixed + */ + public function getBody(); + + public function setCharset(string $charset); + + public function getCharset(): ?string; + + public function setLanguage(string $lang); + + public function getLanguage(): ?string; + + public function getName(): string; + + public function setMaxLineLength(int $lineLength); + + public function getMaxLineLength(): int; + + /** + * Gets this Header rendered as a compliant string. + */ + public function toString(): string; + + /** + * Gets the header's body, prepared for folding into a final header value. + * + * This is not necessarily RFC 2822 compliant since folding white space is + * not added at this stage (see {@link toString()} for that). + */ + public function getBodyAsString(): string; +} diff --git a/plugins/email/vendor/symfony/mime/Header/Headers.php b/plugins/email/vendor/symfony/mime/Header/Headers.php new file mode 100644 index 0000000..8db9125 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/Headers.php @@ -0,0 +1,307 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\LogicException; + +/** + * A collection of headers. + * + * @author Fabien Potencier + */ +final class Headers +{ + private const UNIQUE_HEADERS = [ + 'date', 'from', 'sender', 'reply-to', 'to', 'cc', 'bcc', + 'message-id', 'in-reply-to', 'references', 'subject', + ]; + private const HEADER_CLASS_MAP = [ + 'date' => DateHeader::class, + 'from' => MailboxListHeader::class, + 'sender' => MailboxHeader::class, + 'reply-to' => MailboxListHeader::class, + 'to' => MailboxListHeader::class, + 'cc' => MailboxListHeader::class, + 'bcc' => MailboxListHeader::class, + 'message-id' => IdentificationHeader::class, + 'in-reply-to' => UnstructuredHeader::class, // `In-Reply-To` and `References` are less strict than RFC 2822 (3.6.4) to allow users entering the original email's ... + 'references' => UnstructuredHeader::class, // ... `Message-ID`, even if that is no valid `msg-id` + 'return-path' => PathHeader::class, + ]; + + /** + * @var HeaderInterface[][] + */ + private $headers = []; + private $lineLength = 76; + + public function __construct(HeaderInterface ...$headers) + { + foreach ($headers as $header) { + $this->add($header); + } + } + + public function __clone() + { + foreach ($this->headers as $name => $collection) { + foreach ($collection as $i => $header) { + $this->headers[$name][$i] = clone $header; + } + } + } + + public function setMaxLineLength(int $lineLength) + { + $this->lineLength = $lineLength; + foreach ($this->all() as $header) { + $header->setMaxLineLength($lineLength); + } + } + + public function getMaxLineLength(): int + { + return $this->lineLength; + } + + /** + * @param array $addresses + * + * @return $this + */ + public function addMailboxListHeader(string $name, array $addresses): self + { + return $this->add(new MailboxListHeader($name, Address::createArray($addresses))); + } + + /** + * @param Address|string $address + * + * @return $this + */ + public function addMailboxHeader(string $name, $address): self + { + return $this->add(new MailboxHeader($name, Address::create($address))); + } + + /** + * @param string|array $ids + * + * @return $this + */ + public function addIdHeader(string $name, $ids): self + { + return $this->add(new IdentificationHeader($name, $ids)); + } + + /** + * @param Address|string $path + * + * @return $this + */ + public function addPathHeader(string $name, $path): self + { + return $this->add(new PathHeader($name, $path instanceof Address ? $path : new Address($path))); + } + + /** + * @return $this + */ + public function addDateHeader(string $name, \DateTimeInterface $dateTime): self + { + return $this->add(new DateHeader($name, $dateTime)); + } + + /** + * @return $this + */ + public function addTextHeader(string $name, string $value): self + { + return $this->add(new UnstructuredHeader($name, $value)); + } + + /** + * @return $this + */ + public function addParameterizedHeader(string $name, string $value, array $params = []): self + { + return $this->add(new ParameterizedHeader($name, $value, $params)); + } + + /** + * @return $this + */ + public function addHeader(string $name, $argument, array $more = []): self + { + $parts = explode('\\', self::HEADER_CLASS_MAP[strtolower($name)] ?? UnstructuredHeader::class); + $method = 'add'.ucfirst(array_pop($parts)); + if ('addUnstructuredHeader' === $method) { + $method = 'addTextHeader'; + } elseif ('addIdentificationHeader' === $method) { + $method = 'addIdHeader'; + } + + return $this->$method($name, $argument, $more); + } + + public function has(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + /** + * @return $this + */ + public function add(HeaderInterface $header): self + { + self::checkHeaderClass($header); + + $header->setMaxLineLength($this->lineLength); + $name = strtolower($header->getName()); + + if (\in_array($name, self::UNIQUE_HEADERS, true) && isset($this->headers[$name]) && \count($this->headers[$name]) > 0) { + throw new LogicException(sprintf('Impossible to set header "%s" as it\'s already defined and must be unique.', $header->getName())); + } + + $this->headers[$name][] = $header; + + return $this; + } + + public function get(string $name): ?HeaderInterface + { + $name = strtolower($name); + if (!isset($this->headers[$name])) { + return null; + } + + $values = array_values($this->headers[$name]); + + return array_shift($values); + } + + public function all(string $name = null): iterable + { + if (null === $name) { + foreach ($this->headers as $name => $collection) { + foreach ($collection as $header) { + yield $name => $header; + } + } + } elseif (isset($this->headers[strtolower($name)])) { + foreach ($this->headers[strtolower($name)] as $header) { + yield $header; + } + } + } + + public function getNames(): array + { + return array_keys($this->headers); + } + + public function remove(string $name): void + { + unset($this->headers[strtolower($name)]); + } + + public static function isUniqueHeader(string $name): bool + { + return \in_array(strtolower($name), self::UNIQUE_HEADERS, true); + } + + /** + * @throws LogicException if the header name and class are not compatible + */ + public static function checkHeaderClass(HeaderInterface $header): void + { + $name = strtolower($header->getName()); + + if (($c = self::HEADER_CLASS_MAP[$name] ?? null) && !$header instanceof $c) { + throw new LogicException(sprintf('The "%s" header must be an instance of "%s" (got "%s").', $header->getName(), $c, get_debug_type($header))); + } + } + + public function toString(): string + { + $string = ''; + foreach ($this->toArray() as $str) { + $string .= $str."\r\n"; + } + + return $string; + } + + public function toArray(): array + { + $arr = []; + foreach ($this->all() as $header) { + if ('' !== $header->getBodyAsString()) { + $arr[] = $header->toString(); + } + } + + return $arr; + } + + /** + * @internal + */ + public function getHeaderBody(string $name) + { + return $this->has($name) ? $this->get($name)->getBody() : null; + } + + /** + * @internal + */ + public function setHeaderBody(string $type, string $name, $body): void + { + if ($this->has($name)) { + $this->get($name)->setBody($body); + } else { + $this->{'add'.$type.'Header'}($name, $body); + } + } + + public function getHeaderParameter(string $name, string $parameter): ?string + { + if (!$this->has($name)) { + return null; + } + + $header = $this->get($name); + if (!$header instanceof ParameterizedHeader) { + throw new LogicException(sprintf('Unable to get parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class)); + } + + return $header->getParameter($parameter); + } + + /** + * @internal + */ + public function setHeaderParameter(string $name, string $parameter, ?string $value): void + { + if (!$this->has($name)) { + throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not defined.', $parameter, $name)); + } + + $header = $this->get($name); + if (!$header instanceof ParameterizedHeader) { + throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class)); + } + + $header->setParameter($parameter, $value); + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/IdentificationHeader.php b/plugins/email/vendor/symfony/mime/Header/IdentificationHeader.php new file mode 100644 index 0000000..8a94574 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/IdentificationHeader.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * An ID MIME Header for something like Message-ID or Content-ID (one or more addresses). + * + * @author Chris Corbyn + */ +final class IdentificationHeader extends AbstractHeader +{ + private $ids = []; + private $idsAsAddresses = []; + + /** + * @param string|array $ids + */ + public function __construct(string $name, $ids) + { + parent::__construct($name); + + $this->setId($ids); + } + + /** + * @param string|array $body a string ID or an array of IDs + * + * @throws RfcComplianceException + */ + public function setBody($body) + { + $this->setId($body); + } + + public function getBody(): array + { + return $this->getIds(); + } + + /** + * Set the ID used in the value of this header. + * + * @param string|array $id + * + * @throws RfcComplianceException + */ + public function setId($id) + { + $this->setIds(\is_array($id) ? $id : [$id]); + } + + /** + * Get the ID used in the value of this Header. + * + * If multiple IDs are set only the first is returned. + */ + public function getId(): ?string + { + return $this->ids[0] ?? null; + } + + /** + * Set a collection of IDs to use in the value of this Header. + * + * @param string[] $ids + * + * @throws RfcComplianceException + */ + public function setIds(array $ids) + { + $this->ids = []; + $this->idsAsAddresses = []; + foreach ($ids as $id) { + $this->idsAsAddresses[] = new Address($id); + $this->ids[] = $id; + } + } + + /** + * Get the list of IDs used in this Header. + * + * @return string[] + */ + public function getIds(): array + { + return $this->ids; + } + + public function getBodyAsString(): string + { + $addrs = []; + foreach ($this->idsAsAddresses as $address) { + $addrs[] = '<'.$address->toString().'>'; + } + + return implode(' ', $addrs); + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/MailboxHeader.php b/plugins/email/vendor/symfony/mime/Header/MailboxHeader.php new file mode 100644 index 0000000..b58c825 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/MailboxHeader.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * A Mailbox MIME Header for something like Sender (one named address). + * + * @author Fabien Potencier + */ +final class MailboxHeader extends AbstractHeader +{ + private $address; + + public function __construct(string $name, Address $address) + { + parent::__construct($name); + + $this->setAddress($address); + } + + /** + * @param Address $body + * + * @throws RfcComplianceException + */ + public function setBody($body) + { + $this->setAddress($body); + } + + /** + * @throws RfcComplianceException + */ + public function getBody(): Address + { + return $this->getAddress(); + } + + /** + * @throws RfcComplianceException + */ + public function setAddress(Address $address) + { + $this->address = $address; + } + + public function getAddress(): Address + { + return $this->address; + } + + public function getBodyAsString(): string + { + $str = $this->address->getEncodedAddress(); + if ($name = $this->address->getName()) { + $str = $this->createPhrase($this, $name, $this->getCharset(), true).' <'.$str.'>'; + } + + return $str; + } + + /** + * Redefine the encoding requirements for an address. + * + * All "specials" must be encoded as the full header value will not be quoted + * + * @see RFC 2822 3.2.1 + */ + protected function tokenNeedsEncoding(string $token): bool + { + return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token); + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/MailboxListHeader.php b/plugins/email/vendor/symfony/mime/Header/MailboxListHeader.php new file mode 100644 index 0000000..1d00fdb --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/MailboxListHeader.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * A Mailbox list MIME Header for something like From, To, Cc, and Bcc (one or more named addresses). + * + * @author Chris Corbyn + */ +final class MailboxListHeader extends AbstractHeader +{ + private $addresses = []; + + /** + * @param Address[] $addresses + */ + public function __construct(string $name, array $addresses) + { + parent::__construct($name); + + $this->setAddresses($addresses); + } + + /** + * @param Address[] $body + * + * @throws RfcComplianceException + */ + public function setBody($body) + { + $this->setAddresses($body); + } + + /** + * @throws RfcComplianceException + * + * @return Address[] + */ + public function getBody(): array + { + return $this->getAddresses(); + } + + /** + * Sets a list of addresses to be shown in this Header. + * + * @param Address[] $addresses + * + * @throws RfcComplianceException + */ + public function setAddresses(array $addresses) + { + $this->addresses = []; + $this->addAddresses($addresses); + } + + /** + * Sets a list of addresses to be shown in this Header. + * + * @param Address[] $addresses + * + * @throws RfcComplianceException + */ + public function addAddresses(array $addresses) + { + foreach ($addresses as $address) { + $this->addAddress($address); + } + } + + /** + * @throws RfcComplianceException + */ + public function addAddress(Address $address) + { + $this->addresses[] = $address; + } + + /** + * @return Address[] + */ + public function getAddresses(): array + { + return $this->addresses; + } + + /** + * Gets the full mailbox list of this Header as an array of valid RFC 2822 strings. + * + * @throws RfcComplianceException + * + * @return string[] + */ + public function getAddressStrings(): array + { + $strings = []; + foreach ($this->addresses as $address) { + $str = $address->getEncodedAddress(); + if ($name = $address->getName()) { + $str = $this->createPhrase($this, $name, $this->getCharset(), !$strings).' <'.$str.'>'; + } + $strings[] = $str; + } + + return $strings; + } + + public function getBodyAsString(): string + { + return implode(', ', $this->getAddressStrings()); + } + + /** + * Redefine the encoding requirements for addresses. + * + * All "specials" must be encoded as the full header value will not be quoted + * + * @see RFC 2822 3.2.1 + */ + protected function tokenNeedsEncoding(string $token): bool + { + return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token); + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/ParameterizedHeader.php b/plugins/email/vendor/symfony/mime/Header/ParameterizedHeader.php new file mode 100644 index 0000000..e5d4238 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/ParameterizedHeader.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Encoder\Rfc2231Encoder; + +/** + * @author Chris Corbyn + */ +final class ParameterizedHeader extends UnstructuredHeader +{ + /** + * RFC 2231's definition of a token. + * + * @var string + */ + public const TOKEN_REGEX = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)'; + + private $encoder; + private $parameters = []; + + public function __construct(string $name, string $value, array $parameters = []) + { + parent::__construct($name, $value); + + foreach ($parameters as $k => $v) { + $this->setParameter($k, $v); + } + + if ('content-type' !== strtolower($name)) { + $this->encoder = new Rfc2231Encoder(); + } + } + + public function setParameter(string $parameter, ?string $value) + { + $this->setParameters(array_merge($this->getParameters(), [$parameter => $value])); + } + + public function getParameter(string $parameter): string + { + return $this->getParameters()[$parameter] ?? ''; + } + + /** + * @param string[] $parameters + */ + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + } + + /** + * @return string[] + */ + public function getParameters(): array + { + return $this->parameters; + } + + public function getBodyAsString(): string + { + $body = parent::getBodyAsString(); + foreach ($this->parameters as $name => $value) { + if (null !== $value) { + $body .= '; '.$this->createParameter($name, $value); + } + } + + return $body; + } + + /** + * Generate a list of all tokens in the final header. + * + * This doesn't need to be overridden in theory, but it is for implementation + * reasons to prevent potential breakage of attributes. + */ + protected function toTokens(string $string = null): array + { + $tokens = parent::toTokens(parent::getBodyAsString()); + + // Try creating any parameters + foreach ($this->parameters as $name => $value) { + if (null !== $value) { + // Add the semi-colon separator + $tokens[\count($tokens) - 1] .= ';'; + $tokens = array_merge($tokens, $this->generateTokenLines(' '.$this->createParameter($name, $value))); + } + } + + return $tokens; + } + + /** + * Render an RFC 2047 compliant header parameter from the $name and $value. + */ + private function createParameter(string $name, string $value): string + { + $origValue = $value; + + $encoded = false; + // Allow room for parameter name, indices, "=" and DQUOTEs + $maxValueLength = $this->getMaxLineLength() - \strlen($name.'=*N"";') - 1; + $firstLineOffset = 0; + + // If it's not already a valid parameter value... + if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) { + // TODO: text, or something else?? + // ... and it's not ascii + if (!preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $value)) { + $encoded = true; + // Allow space for the indices, charset and language + $maxValueLength = $this->getMaxLineLength() - \strlen($name.'*N*="";') - 1; + $firstLineOffset = \strlen($this->getCharset()."'".$this->getLanguage()."'"); + } + + if (\in_array($name, ['name', 'filename'], true) && 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName()) && preg_match('//u', $value)) { + // WHATWG HTML living standard 4.10.21.8 2 specifies: + // For field names and filenames for file fields, the result of the + // encoding in the previous bullet point must be escaped by replacing + // any 0x0A (LF) bytes with the byte sequence `%0A`, 0x0D (CR) with `%0D` + // and 0x22 (") with `%22`. + // The user agent must not perform any other escapes. + $value = str_replace(['"', "\r", "\n"], ['%22', '%0D', '%0A'], $value); + + if (\strlen($value) <= $maxValueLength) { + return $name.'="'.$value.'"'; + } + + $value = $origValue; + } + } + + // Encode if we need to + if ($encoded || \strlen($value) > $maxValueLength) { + if (null !== $this->encoder) { + $value = $this->encoder->encodeString($origValue, $this->getCharset(), $firstLineOffset, $maxValueLength); + } else { + // We have to go against RFC 2183/2231 in some areas for interoperability + $value = $this->getTokenAsEncodedWord($origValue); + $encoded = false; + } + } + + $valueLines = $this->encoder ? explode("\r\n", $value) : [$value]; + + // Need to add indices + if (\count($valueLines) > 1) { + $paramLines = []; + foreach ($valueLines as $i => $line) { + $paramLines[] = $name.'*'.$i.$this->getEndOfParameterValue($line, true, 0 === $i); + } + + return implode(";\r\n ", $paramLines); + } else { + return $name.$this->getEndOfParameterValue($valueLines[0], $encoded, true); + } + } + + /** + * Returns the parameter value from the "=" and beyond. + * + * @param string $value to append + */ + private function getEndOfParameterValue(string $value, bool $encoded = false, bool $firstLine = false): string + { + $forceHttpQuoting = 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName()); + if ($forceHttpQuoting || !preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) { + $value = '"'.$value.'"'; + } + $prepend = '='; + if ($encoded) { + $prepend = '*='; + if ($firstLine) { + $prepend = '*='.$this->getCharset()."'".$this->getLanguage()."'"; + } + } + + return $prepend.$value; + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/PathHeader.php b/plugins/email/vendor/symfony/mime/Header/PathHeader.php new file mode 100644 index 0000000..5101ad0 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/PathHeader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * A Path Header, such a Return-Path (one address). + * + * @author Chris Corbyn + */ +final class PathHeader extends AbstractHeader +{ + private $address; + + public function __construct(string $name, Address $address) + { + parent::__construct($name); + + $this->setAddress($address); + } + + /** + * @param Address $body + * + * @throws RfcComplianceException + */ + public function setBody($body) + { + $this->setAddress($body); + } + + public function getBody(): Address + { + return $this->getAddress(); + } + + public function setAddress(Address $address) + { + $this->address = $address; + } + + public function getAddress(): Address + { + return $this->address; + } + + public function getBodyAsString(): string + { + return '<'.$this->address->toString().'>'; + } +} diff --git a/plugins/email/vendor/symfony/mime/Header/UnstructuredHeader.php b/plugins/email/vendor/symfony/mime/Header/UnstructuredHeader.php new file mode 100644 index 0000000..2085ddf --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Header/UnstructuredHeader.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +/** + * A Simple MIME Header. + * + * @author Chris Corbyn + */ +class UnstructuredHeader extends AbstractHeader +{ + private $value; + + public function __construct(string $name, string $value) + { + parent::__construct($name); + + $this->setValue($value); + } + + /** + * @param string $body + */ + public function setBody($body) + { + $this->setValue($body); + } + + /** + * @return string + */ + public function getBody() + { + return $this->getValue(); + } + + /** + * Get the (unencoded) value of this header. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Set the (unencoded) value of this header. + */ + public function setValue(string $value) + { + $this->value = $value; + } + + /** + * Get the value of this header prepared for rendering. + */ + public function getBodyAsString(): string + { + return $this->encodeWords($this, $this->value); + } +} diff --git a/plugins/email/vendor/symfony/mime/LICENSE b/plugins/email/vendor/symfony/mime/LICENSE new file mode 100644 index 0000000..298be14 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/mime/Message.php b/plugins/email/vendor/symfony/mime/Message.php new file mode 100644 index 0000000..651ffd4 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Message.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @author Fabien Potencier + */ +class Message extends RawMessage +{ + private $headers; + private $body; + + public function __construct(Headers $headers = null, AbstractPart $body = null) + { + $this->headers = $headers ? clone $headers : new Headers(); + $this->body = $body; + } + + public function __clone() + { + $this->headers = clone $this->headers; + + if (null !== $this->body) { + $this->body = clone $this->body; + } + } + + /** + * @return $this + */ + public function setBody(AbstractPart $body = null) + { + $this->body = $body; + + return $this; + } + + public function getBody(): ?AbstractPart + { + return $this->body; + } + + /** + * @return $this + */ + public function setHeaders(Headers $headers) + { + $this->headers = $headers; + + return $this; + } + + public function getHeaders(): Headers + { + return $this->headers; + } + + public function getPreparedHeaders(): Headers + { + $headers = clone $this->headers; + + if (!$headers->has('From')) { + if (!$headers->has('Sender')) { + throw new LogicException('An email must have a "From" or a "Sender" header.'); + } + $headers->addMailboxListHeader('From', [$headers->get('Sender')->getAddress()]); + } + + if (!$headers->has('MIME-Version')) { + $headers->addTextHeader('MIME-Version', '1.0'); + } + + if (!$headers->has('Date')) { + $headers->addDateHeader('Date', new \DateTimeImmutable()); + } + + // determine the "real" sender + if (!$headers->has('Sender') && \count($froms = $headers->get('From')->getAddresses()) > 1) { + $headers->addMailboxHeader('Sender', $froms[0]); + } + + if (!$headers->has('Message-ID')) { + $headers->addIdHeader('Message-ID', $this->generateMessageId()); + } + + // remove the Bcc field which should NOT be part of the sent message + $headers->remove('Bcc'); + + return $headers; + } + + public function toString(): string + { + if (null === $body = $this->getBody()) { + $body = new TextPart(''); + } + + return $this->getPreparedHeaders()->toString().$body->toString(); + } + + public function toIterable(): iterable + { + if (null === $body = $this->getBody()) { + $body = new TextPart(''); + } + + yield $this->getPreparedHeaders()->toString(); + yield from $body->toIterable(); + } + + public function ensureValidity() + { + if (!$this->headers->has('To') && !$this->headers->has('Cc') && !$this->headers->has('Bcc')) { + throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.'); + } + + if (!$this->headers->has('From') && !$this->headers->has('Sender')) { + throw new LogicException('An email must have a "From" or a "Sender" header.'); + } + + parent::ensureValidity(); + } + + public function generateMessageId(): string + { + if ($this->headers->has('Sender')) { + $sender = $this->headers->get('Sender')->getAddress(); + } elseif ($this->headers->has('From')) { + $sender = $this->headers->get('From')->getAddresses()[0]; + } else { + throw new LogicException('An email must have a "From" or a "Sender" header.'); + } + + return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@'); + } + + public function __serialize(): array + { + return [$this->headers, $this->body]; + } + + public function __unserialize(array $data): void + { + [$this->headers, $this->body] = $data; + } +} diff --git a/plugins/email/vendor/symfony/mime/MessageConverter.php b/plugins/email/vendor/symfony/mime/MessageConverter.php new file mode 100644 index 0000000..0539eac --- /dev/null +++ b/plugins/email/vendor/symfony/mime/MessageConverter.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\Multipart\AlternativePart; +use Symfony\Component\Mime\Part\Multipart\MixedPart; +use Symfony\Component\Mime\Part\Multipart\RelatedPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @author Fabien Potencier + */ +final class MessageConverter +{ + /** + * @throws RuntimeException when unable to convert the message to an email + */ + public static function toEmail(Message $message): Email + { + if ($message instanceof Email) { + return $message; + } + + // try to convert to a "simple" Email instance + $body = $message->getBody(); + if ($body instanceof TextPart) { + return self::createEmailFromTextPart($message, $body); + } + + if ($body instanceof AlternativePart) { + return self::createEmailFromAlternativePart($message, $body); + } + + if ($body instanceof RelatedPart) { + return self::createEmailFromRelatedPart($message, $body); + } + + if ($body instanceof MixedPart) { + $parts = $body->getParts(); + if ($parts[0] instanceof RelatedPart) { + $email = self::createEmailFromRelatedPart($message, $parts[0]); + } elseif ($parts[0] instanceof AlternativePart) { + $email = self::createEmailFromAlternativePart($message, $parts[0]); + } elseif ($parts[0] instanceof TextPart) { + $email = self::createEmailFromTextPart($message, $parts[0]); + } else { + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + return self::attachParts($email, \array_slice($parts, 1)); + } + + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + private static function createEmailFromTextPart(Message $message, TextPart $part): Email + { + if ('text' === $part->getMediaType() && 'plain' === $part->getMediaSubtype()) { + return (new Email(clone $message->getHeaders()))->text($part->getBody(), $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8'); + } + if ('text' === $part->getMediaType() && 'html' === $part->getMediaSubtype()) { + return (new Email(clone $message->getHeaders()))->html($part->getBody(), $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8'); + } + + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + private static function createEmailFromAlternativePart(Message $message, AlternativePart $part): Email + { + $parts = $part->getParts(); + if ( + 2 === \count($parts) && + $parts[0] instanceof TextPart && 'text' === $parts[0]->getMediaType() && 'plain' === $parts[0]->getMediaSubtype() && + $parts[1] instanceof TextPart && 'text' === $parts[1]->getMediaType() && 'html' === $parts[1]->getMediaSubtype() + ) { + return (new Email(clone $message->getHeaders())) + ->text($parts[0]->getBody(), $parts[0]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8') + ->html($parts[1]->getBody(), $parts[1]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8') + ; + } + + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + private static function createEmailFromRelatedPart(Message $message, RelatedPart $part): Email + { + $parts = $part->getParts(); + if ($parts[0] instanceof AlternativePart) { + $email = self::createEmailFromAlternativePart($message, $parts[0]); + } elseif ($parts[0] instanceof TextPart) { + $email = self::createEmailFromTextPart($message, $parts[0]); + } else { + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + return self::attachParts($email, \array_slice($parts, 1)); + } + + private static function attachParts(Email $email, array $parts): Email + { + foreach ($parts as $part) { + if (!$part instanceof DataPart) { + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($email))); + } + + $headers = $part->getPreparedHeaders(); + $method = 'inline' === $headers->getHeaderBody('Content-Disposition') ? 'embed' : 'attach'; + $name = $headers->getHeaderParameter('Content-Disposition', 'filename'); + $email->$method($part->getBody(), $name, $part->getMediaType().'/'.$part->getMediaSubtype()); + } + + return $email; + } +} diff --git a/plugins/email/vendor/symfony/mime/MimeTypeGuesserInterface.php b/plugins/email/vendor/symfony/mime/MimeTypeGuesserInterface.php new file mode 100644 index 0000000..30ee3b6 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/MimeTypeGuesserInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * Guesses the MIME type of a file. + * + * @author Fabien Potencier + */ +interface MimeTypeGuesserInterface +{ + /** + * Returns true if this guesser is supported. + */ + public function isGuesserSupported(): bool; + + /** + * Guesses the MIME type of the file with the given path. + * + * @throws \LogicException If the guesser is not supported + * @throws \InvalidArgumentException If the file does not exist or is not readable + */ + public function guessMimeType(string $path): ?string; +} diff --git a/plugins/email/vendor/symfony/mime/MimeTypes.php b/plugins/email/vendor/symfony/mime/MimeTypes.php new file mode 100644 index 0000000..bdea994 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/MimeTypes.php @@ -0,0 +1,3537 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; + +/** + * Manages MIME types and file extensions. + * + * For MIME type guessing, you can register custom guessers + * by calling the registerGuesser() method. + * Custom guessers are always called before any default ones: + * + * $guesser = new MimeTypes(); + * $guesser->registerGuesser(new MyCustomMimeTypeGuesser()); + * + * If you want to change the order of the default guessers, just re-register your + * preferred one as a custom one. The last registered guesser is preferred over + * previously registered ones. + * + * Re-registering a built-in guesser also allows you to configure it: + * + * $guesser = new MimeTypes(); + * $guesser->registerGuesser(new FileinfoMimeTypeGuesser('/path/to/magic/file')); + * + * @author Fabien Potencier + */ +final class MimeTypes implements MimeTypesInterface +{ + private $extensions = []; + private $mimeTypes = []; + + /** + * @var MimeTypeGuesserInterface[] + */ + private $guessers = []; + private static $default; + + public function __construct(array $map = []) + { + foreach ($map as $mimeType => $extensions) { + $this->extensions[$mimeType] = $extensions; + + foreach ($extensions as $extension) { + $this->mimeTypes[$extension][] = $mimeType; + } + } + $this->registerGuesser(new FileBinaryMimeTypeGuesser()); + $this->registerGuesser(new FileinfoMimeTypeGuesser()); + } + + public static function setDefault(self $default) + { + self::$default = $default; + } + + public static function getDefault(): self + { + return self::$default ?? self::$default = new self(); + } + + /** + * Registers a MIME type guesser. + * + * The last registered guesser has precedence over the other ones. + */ + public function registerGuesser(MimeTypeGuesserInterface $guesser) + { + array_unshift($this->guessers, $guesser); + } + + /** + * {@inheritdoc} + */ + public function getExtensions(string $mimeType): array + { + if ($this->extensions) { + $extensions = $this->extensions[$mimeType] ?? $this->extensions[$lcMimeType = strtolower($mimeType)] ?? null; + } + + return $extensions ?? self::MAP[$mimeType] ?? self::MAP[$lcMimeType ?? strtolower($mimeType)] ?? []; + } + + /** + * {@inheritdoc} + */ + public function getMimeTypes(string $ext): array + { + if ($this->mimeTypes) { + $mimeTypes = $this->mimeTypes[$ext] ?? $this->mimeTypes[$lcExt = strtolower($ext)] ?? null; + } + + return $mimeTypes ?? self::REVERSE_MAP[$ext] ?? self::REVERSE_MAP[$lcExt ?? strtolower($ext)] ?? []; + } + + /** + * {@inheritdoc} + */ + public function isGuesserSupported(): bool + { + foreach ($this->guessers as $guesser) { + if ($guesser->isGuesserSupported()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + * + * The file is passed to each registered MIME type guesser in reverse order + * of their registration (last registered is queried first). Once a guesser + * returns a value that is not null, this method terminates and returns the + * value. + */ + public function guessMimeType(string $path): ?string + { + foreach ($this->guessers as $guesser) { + if (!$guesser->isGuesserSupported()) { + continue; + } + + if (null !== $mimeType = $guesser->guessMimeType($path)) { + return $mimeType; + } + } + + if (!$this->isGuesserSupported()) { + throw new LogicException('Unable to guess the MIME type as no guessers are available (have you enabled the php_fileinfo extension?).'); + } + + return null; + } + + /** + * A map of MIME types and their default extensions. + * + * Updated from upstream on 2021-09-03 + * + * @see Resources/bin/update_mime_types.php + */ + private const MAP = [ + 'application/acrobat' => ['pdf'], + 'application/andrew-inset' => ['ez'], + 'application/annodex' => ['anx'], + 'application/applixware' => ['aw'], + 'application/atom+xml' => ['atom'], + 'application/atomcat+xml' => ['atomcat'], + 'application/atomdeleted+xml' => ['atomdeleted'], + 'application/atomsvc+xml' => ['atomsvc'], + 'application/atsc-dwd+xml' => ['dwd'], + 'application/atsc-held+xml' => ['held'], + 'application/atsc-rsat+xml' => ['rsat'], + 'application/bdoc' => ['bdoc'], + 'application/bzip2' => ['bz2', 'bz'], + 'application/calendar+xml' => ['xcs'], + 'application/ccxml+xml' => ['ccxml'], + 'application/cdfx+xml' => ['cdfx'], + 'application/cdmi-capability' => ['cdmia'], + 'application/cdmi-container' => ['cdmic'], + 'application/cdmi-domain' => ['cdmid'], + 'application/cdmi-object' => ['cdmio'], + 'application/cdmi-queue' => ['cdmiq'], + 'application/cdr' => ['cdr'], + 'application/coreldraw' => ['cdr'], + 'application/csv' => ['csv'], + 'application/cu-seeme' => ['cu'], + 'application/dash+xml' => ['mpd'], + 'application/davmount+xml' => ['davmount'], + 'application/dbase' => ['dbf'], + 'application/dbf' => ['dbf'], + 'application/dicom' => ['dcm'], + 'application/docbook+xml' => ['dbk', 'docbook'], + 'application/dssc+der' => ['dssc'], + 'application/dssc+xml' => ['xdssc'], + 'application/ecmascript' => ['ecma', 'es'], + 'application/emf' => ['emf'], + 'application/emma+xml' => ['emma'], + 'application/emotionml+xml' => ['emotionml'], + 'application/epub+zip' => ['epub'], + 'application/exi' => ['exi'], + 'application/fdt+xml' => ['fdt'], + 'application/font-tdpfr' => ['pfr'], + 'application/font-woff' => ['woff'], + 'application/futuresplash' => ['swf', 'spl'], + 'application/geo+json' => ['geojson', 'geo.json'], + 'application/gml+xml' => ['gml'], + 'application/gnunet-directory' => ['gnd'], + 'application/gpx' => ['gpx'], + 'application/gpx+xml' => ['gpx'], + 'application/gxf' => ['gxf'], + 'application/gzip' => ['gz'], + 'application/hjson' => ['hjson'], + 'application/hyperstudio' => ['stk'], + 'application/ico' => ['ico'], + 'application/ics' => ['vcs', 'ics'], + 'application/illustrator' => ['ai'], + 'application/inkml+xml' => ['ink', 'inkml'], + 'application/ipfix' => ['ipfix'], + 'application/its+xml' => ['its'], + 'application/java' => ['class'], + 'application/java-archive' => ['jar', 'war', 'ear'], + 'application/java-byte-code' => ['class'], + 'application/java-serialized-object' => ['ser'], + 'application/java-vm' => ['class'], + 'application/javascript' => ['js', 'mjs', 'jsm'], + 'application/jrd+json' => ['jrd'], + 'application/json' => ['json', 'map'], + 'application/json-patch+json' => ['json-patch'], + 'application/json5' => ['json5'], + 'application/jsonml+json' => ['jsonml'], + 'application/ld+json' => ['jsonld'], + 'application/lgr+xml' => ['lgr'], + 'application/lost+xml' => ['lostxml'], + 'application/lotus123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/m3u' => ['m3u', 'm3u8', 'vlc'], + 'application/mac-binhex40' => ['hqx'], + 'application/mac-compactpro' => ['cpt'], + 'application/mads+xml' => ['mads'], + 'application/manifest+json' => ['webmanifest'], + 'application/marc' => ['mrc'], + 'application/marcxml+xml' => ['mrcx'], + 'application/mathematica' => ['ma', 'nb', 'mb'], + 'application/mathml+xml' => ['mathml', 'mml'], + 'application/mbox' => ['mbox'], + 'application/mdb' => ['mdb'], + 'application/mediaservercontrol+xml' => ['mscml'], + 'application/metalink+xml' => ['metalink'], + 'application/metalink4+xml' => ['meta4'], + 'application/mets+xml' => ['mets'], + 'application/mmt-aei+xml' => ['maei'], + 'application/mmt-usd+xml' => ['musd'], + 'application/mods+xml' => ['mods'], + 'application/mp21' => ['m21', 'mp21'], + 'application/mp4' => ['mp4s', 'm4p'], + 'application/mrb-consumer+xml' => ['xdf'], + 'application/mrb-publish+xml' => ['xdf'], + 'application/ms-tnef' => ['tnef', 'tnf'], + 'application/msaccess' => ['mdb'], + 'application/msexcel' => ['xls', 'xlc', 'xll', 'xlm', 'xlw', 'xla', 'xlt', 'xld'], + 'application/mspowerpoint' => ['ppz', 'ppt', 'pps', 'pot'], + 'application/msword' => ['doc', 'dot'], + 'application/msword-template' => ['dot'], + 'application/mxf' => ['mxf'], + 'application/n-quads' => ['nq'], + 'application/n-triples' => ['nt'], + 'application/nappdf' => ['pdf'], + 'application/node' => ['cjs'], + 'application/octet-stream' => ['bin', 'dms', 'lrf', 'mar', 'so', 'dist', 'distz', 'pkg', 'bpk', 'dump', 'elc', 'deploy', 'exe', 'dll', 'deb', 'dmg', 'iso', 'img', 'msi', 'msp', 'msm', 'buffer'], + 'application/oda' => ['oda'], + 'application/oebps-package+xml' => ['opf'], + 'application/ogg' => ['ogx'], + 'application/omdoc+xml' => ['omdoc'], + 'application/onenote' => ['onetoc', 'onetoc2', 'onetmp', 'onepkg'], + 'application/ovf' => ['ova'], + 'application/owl+xml' => ['owx'], + 'application/oxps' => ['oxps'], + 'application/p2p-overlay+xml' => ['relo'], + 'application/patch-ops-error+xml' => ['xer'], + 'application/pcap' => ['pcap', 'cap', 'dmp'], + 'application/pdf' => ['pdf'], + 'application/pgp' => ['pgp', 'gpg', 'asc'], + 'application/pgp-encrypted' => ['pgp', 'gpg', 'asc'], + 'application/pgp-keys' => ['skr', 'pkr', 'asc', 'pgp', 'gpg', 'key'], + 'application/pgp-signature' => ['asc', 'sig', 'pgp', 'gpg'], + 'application/photoshop' => ['psd'], + 'application/pics-rules' => ['prf'], + 'application/pkcs10' => ['p10'], + 'application/pkcs12' => ['p12', 'pfx'], + 'application/pkcs7-mime' => ['p7m', 'p7c'], + 'application/pkcs7-signature' => ['p7s'], + 'application/pkcs8' => ['p8'], + 'application/pkcs8-encrypted' => ['p8e'], + 'application/pkix-attr-cert' => ['ac'], + 'application/pkix-cert' => ['cer'], + 'application/pkix-crl' => ['crl'], + 'application/pkix-pkipath' => ['pkipath'], + 'application/pkixcmp' => ['pki'], + 'application/pls' => ['pls'], + 'application/pls+xml' => ['pls'], + 'application/postscript' => ['ai', 'eps', 'ps'], + 'application/powerpoint' => ['ppz', 'ppt', 'pps', 'pot'], + 'application/provenance+xml' => ['provx'], + 'application/prs.cww' => ['cww'], + 'application/pskc+xml' => ['pskcxml'], + 'application/ram' => ['ram'], + 'application/raml+yaml' => ['raml'], + 'application/rdf+xml' => ['rdf', 'owl', 'rdfs'], + 'application/reginfo+xml' => ['rif'], + 'application/relax-ng-compact-syntax' => ['rnc'], + 'application/resource-lists+xml' => ['rl'], + 'application/resource-lists-diff+xml' => ['rld'], + 'application/rls-services+xml' => ['rs'], + 'application/route-apd+xml' => ['rapd'], + 'application/route-s-tsid+xml' => ['sls'], + 'application/route-usd+xml' => ['rusd'], + 'application/rpki-ghostbusters' => ['gbr'], + 'application/rpki-manifest' => ['mft'], + 'application/rpki-roa' => ['roa'], + 'application/rsd+xml' => ['rsd'], + 'application/rss+xml' => ['rss'], + 'application/rtf' => ['rtf'], + 'application/sbml+xml' => ['sbml'], + 'application/schema+json' => ['json'], + 'application/scvp-cv-request' => ['scq'], + 'application/scvp-cv-response' => ['scs'], + 'application/scvp-vp-request' => ['spq'], + 'application/scvp-vp-response' => ['spp'], + 'application/sdp' => ['sdp'], + 'application/senml+xml' => ['senmlx'], + 'application/sensml+xml' => ['sensmlx'], + 'application/set-payment-initiation' => ['setpay'], + 'application/set-registration-initiation' => ['setreg'], + 'application/shf+xml' => ['shf'], + 'application/sieve' => ['siv', 'sieve'], + 'application/smil' => ['smil', 'smi', 'sml', 'kino'], + 'application/smil+xml' => ['smi', 'smil', 'sml', 'kino'], + 'application/sparql-query' => ['rq'], + 'application/sparql-results+xml' => ['srx'], + 'application/sql' => ['sql'], + 'application/srgs' => ['gram'], + 'application/srgs+xml' => ['grxml'], + 'application/sru+xml' => ['sru'], + 'application/ssdl+xml' => ['ssdl'], + 'application/ssml+xml' => ['ssml'], + 'application/stuffit' => ['sit', 'hqx'], + 'application/swid+xml' => ['swidtag'], + 'application/tei+xml' => ['tei', 'teicorpus'], + 'application/tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'application/thraud+xml' => ['tfi'], + 'application/timestamped-data' => ['tsd'], + 'application/toml' => ['toml'], + 'application/trig' => ['trig'], + 'application/ttml+xml' => ['ttml'], + 'application/ubjson' => ['ubj'], + 'application/urc-ressheet+xml' => ['rsheet'], + 'application/urc-targetdesc+xml' => ['td'], + 'application/vnd.1000minds.decision-model+xml' => ['1km'], + 'application/vnd.3gpp.pic-bw-large' => ['plb'], + 'application/vnd.3gpp.pic-bw-small' => ['psb'], + 'application/vnd.3gpp.pic-bw-var' => ['pvb'], + 'application/vnd.3gpp2.tcap' => ['tcap'], + 'application/vnd.3m.post-it-notes' => ['pwn'], + 'application/vnd.accpac.simply.aso' => ['aso'], + 'application/vnd.accpac.simply.imp' => ['imp'], + 'application/vnd.acucobol' => ['acu'], + 'application/vnd.acucorp' => ['atc', 'acutc'], + 'application/vnd.adobe.air-application-installer-package+zip' => ['air'], + 'application/vnd.adobe.flash.movie' => ['swf', 'spl'], + 'application/vnd.adobe.formscentral.fcdt' => ['fcdt'], + 'application/vnd.adobe.fxp' => ['fxp', 'fxpl'], + 'application/vnd.adobe.illustrator' => ['ai'], + 'application/vnd.adobe.xdp+xml' => ['xdp'], + 'application/vnd.adobe.xfdf' => ['xfdf'], + 'application/vnd.ahead.space' => ['ahead'], + 'application/vnd.airzip.filesecure.azf' => ['azf'], + 'application/vnd.airzip.filesecure.azs' => ['azs'], + 'application/vnd.amazon.ebook' => ['azw'], + 'application/vnd.amazon.mobi8-ebook' => ['azw3', 'kfx'], + 'application/vnd.americandynamics.acc' => ['acc'], + 'application/vnd.amiga.ami' => ['ami'], + 'application/vnd.android.package-archive' => ['apk'], + 'application/vnd.anser-web-certificate-issue-initiation' => ['cii'], + 'application/vnd.anser-web-funds-transfer-initiation' => ['fti'], + 'application/vnd.antix.game-component' => ['atx'], + 'application/vnd.appimage' => ['appimage'], + 'application/vnd.apple.installer+xml' => ['mpkg'], + 'application/vnd.apple.keynote' => ['key', 'keynote'], + 'application/vnd.apple.mpegurl' => ['m3u8', 'm3u'], + 'application/vnd.apple.numbers' => ['numbers'], + 'application/vnd.apple.pages' => ['pages'], + 'application/vnd.apple.pkpass' => ['pkpass'], + 'application/vnd.aristanetworks.swi' => ['swi'], + 'application/vnd.astraea-software.iota' => ['iota'], + 'application/vnd.audiograph' => ['aep'], + 'application/vnd.balsamiq.bmml+xml' => ['bmml'], + 'application/vnd.blueice.multipass' => ['mpm'], + 'application/vnd.bmi' => ['bmi'], + 'application/vnd.businessobjects' => ['rep'], + 'application/vnd.chemdraw+xml' => ['cdxml'], + 'application/vnd.chess-pgn' => ['pgn'], + 'application/vnd.chipnuts.karaoke-mmd' => ['mmd'], + 'application/vnd.cinderella' => ['cdy'], + 'application/vnd.citationstyles.style+xml' => ['csl'], + 'application/vnd.claymore' => ['cla'], + 'application/vnd.cloanto.rp9' => ['rp9'], + 'application/vnd.clonk.c4group' => ['c4g', 'c4d', 'c4f', 'c4p', 'c4u'], + 'application/vnd.cluetrust.cartomobile-config' => ['c11amc'], + 'application/vnd.cluetrust.cartomobile-config-pkg' => ['c11amz'], + 'application/vnd.coffeescript' => ['coffee'], + 'application/vnd.comicbook+zip' => ['cbz'], + 'application/vnd.comicbook-rar' => ['cbr'], + 'application/vnd.commonspace' => ['csp'], + 'application/vnd.contact.cmsg' => ['cdbcmsg'], + 'application/vnd.corel-draw' => ['cdr'], + 'application/vnd.cosmocaller' => ['cmc'], + 'application/vnd.crick.clicker' => ['clkx'], + 'application/vnd.crick.clicker.keyboard' => ['clkk'], + 'application/vnd.crick.clicker.palette' => ['clkp'], + 'application/vnd.crick.clicker.template' => ['clkt'], + 'application/vnd.crick.clicker.wordbank' => ['clkw'], + 'application/vnd.criticaltools.wbs+xml' => ['wbs'], + 'application/vnd.ctc-posml' => ['pml'], + 'application/vnd.cups-ppd' => ['ppd'], + 'application/vnd.curl.car' => ['car'], + 'application/vnd.curl.pcurl' => ['pcurl'], + 'application/vnd.dart' => ['dart'], + 'application/vnd.data-vision.rdz' => ['rdz'], + 'application/vnd.dbf' => ['dbf'], + 'application/vnd.debian.binary-package' => ['deb', 'udeb'], + 'application/vnd.dece.data' => ['uvf', 'uvvf', 'uvd', 'uvvd'], + 'application/vnd.dece.ttml+xml' => ['uvt', 'uvvt'], + 'application/vnd.dece.unspecified' => ['uvx', 'uvvx'], + 'application/vnd.dece.zip' => ['uvz', 'uvvz'], + 'application/vnd.denovo.fcselayout-link' => ['fe_launch'], + 'application/vnd.dna' => ['dna'], + 'application/vnd.dolby.mlp' => ['mlp'], + 'application/vnd.dpgraph' => ['dpg'], + 'application/vnd.dreamfactory' => ['dfac'], + 'application/vnd.ds-keypoint' => ['kpxx'], + 'application/vnd.dvb.ait' => ['ait'], + 'application/vnd.dvb.service' => ['svc'], + 'application/vnd.dynageo' => ['geo'], + 'application/vnd.ecowin.chart' => ['mag'], + 'application/vnd.emusic-emusic_package' => ['emp'], + 'application/vnd.enliven' => ['nml'], + 'application/vnd.epson.esf' => ['esf'], + 'application/vnd.epson.msf' => ['msf'], + 'application/vnd.epson.quickanime' => ['qam'], + 'application/vnd.epson.salt' => ['slt'], + 'application/vnd.epson.ssf' => ['ssf'], + 'application/vnd.eszigno3+xml' => ['es3', 'et3'], + 'application/vnd.etsi.asic-e+zip' => ['asice'], + 'application/vnd.ezpix-album' => ['ez2'], + 'application/vnd.ezpix-package' => ['ez3'], + 'application/vnd.fdf' => ['fdf'], + 'application/vnd.fdsn.mseed' => ['mseed'], + 'application/vnd.fdsn.seed' => ['seed', 'dataless'], + 'application/vnd.flatpak' => ['flatpak', 'xdgapp'], + 'application/vnd.flatpak.ref' => ['flatpakref'], + 'application/vnd.flatpak.repo' => ['flatpakrepo'], + 'application/vnd.flographit' => ['gph'], + 'application/vnd.fluxtime.clip' => ['ftc'], + 'application/vnd.framemaker' => ['fm', 'frame', 'maker', 'book'], + 'application/vnd.frogans.fnc' => ['fnc'], + 'application/vnd.frogans.ltf' => ['ltf'], + 'application/vnd.fsc.weblaunch' => ['fsc'], + 'application/vnd.fujitsu.oasys' => ['oas'], + 'application/vnd.fujitsu.oasys2' => ['oa2'], + 'application/vnd.fujitsu.oasys3' => ['oa3'], + 'application/vnd.fujitsu.oasysgp' => ['fg5'], + 'application/vnd.fujitsu.oasysprs' => ['bh2'], + 'application/vnd.fujixerox.ddd' => ['ddd'], + 'application/vnd.fujixerox.docuworks' => ['xdw'], + 'application/vnd.fujixerox.docuworks.binder' => ['xbd'], + 'application/vnd.fuzzysheet' => ['fzs'], + 'application/vnd.genomatix.tuxedo' => ['txd'], + 'application/vnd.geo+json' => ['geojson', 'geo.json'], + 'application/vnd.geogebra.file' => ['ggb'], + 'application/vnd.geogebra.tool' => ['ggt'], + 'application/vnd.geometry-explorer' => ['gex', 'gre'], + 'application/vnd.geonext' => ['gxt'], + 'application/vnd.geoplan' => ['g2w'], + 'application/vnd.geospace' => ['g3w'], + 'application/vnd.gmx' => ['gmx'], + 'application/vnd.google-apps.document' => ['gdoc'], + 'application/vnd.google-apps.presentation' => ['gslides'], + 'application/vnd.google-apps.spreadsheet' => ['gsheet'], + 'application/vnd.google-earth.kml+xml' => ['kml'], + 'application/vnd.google-earth.kmz' => ['kmz'], + 'application/vnd.grafeq' => ['gqf', 'gqs'], + 'application/vnd.groove-account' => ['gac'], + 'application/vnd.groove-help' => ['ghf'], + 'application/vnd.groove-identity-message' => ['gim'], + 'application/vnd.groove-injector' => ['grv'], + 'application/vnd.groove-tool-message' => ['gtm'], + 'application/vnd.groove-tool-template' => ['tpl'], + 'application/vnd.groove-vcard' => ['vcg'], + 'application/vnd.haansoft-hwp' => ['hwp'], + 'application/vnd.haansoft-hwt' => ['hwt'], + 'application/vnd.hal+xml' => ['hal'], + 'application/vnd.handheld-entertainment+xml' => ['zmm'], + 'application/vnd.hbci' => ['hbci'], + 'application/vnd.hhe.lesson-player' => ['les'], + 'application/vnd.hp-hpgl' => ['hpgl'], + 'application/vnd.hp-hpid' => ['hpid'], + 'application/vnd.hp-hps' => ['hps'], + 'application/vnd.hp-jlyt' => ['jlt'], + 'application/vnd.hp-pcl' => ['pcl'], + 'application/vnd.hp-pclxl' => ['pclxl'], + 'application/vnd.hydrostatix.sof-data' => ['sfd-hdstx'], + 'application/vnd.ibm.minipay' => ['mpy'], + 'application/vnd.ibm.modcap' => ['afp', 'listafp', 'list3820'], + 'application/vnd.ibm.rights-management' => ['irm'], + 'application/vnd.ibm.secure-container' => ['sc'], + 'application/vnd.iccprofile' => ['icc', 'icm'], + 'application/vnd.igloader' => ['igl'], + 'application/vnd.immervision-ivp' => ['ivp'], + 'application/vnd.immervision-ivu' => ['ivu'], + 'application/vnd.insors.igm' => ['igm'], + 'application/vnd.intercon.formnet' => ['xpw', 'xpx'], + 'application/vnd.intergeo' => ['i2g'], + 'application/vnd.intu.qbo' => ['qbo'], + 'application/vnd.intu.qfx' => ['qfx'], + 'application/vnd.ipunplugged.rcprofile' => ['rcprofile'], + 'application/vnd.irepository.package+xml' => ['irp'], + 'application/vnd.is-xpr' => ['xpr'], + 'application/vnd.isac.fcs' => ['fcs'], + 'application/vnd.jam' => ['jam'], + 'application/vnd.jcp.javame.midlet-rms' => ['rms'], + 'application/vnd.jisp' => ['jisp'], + 'application/vnd.joost.joda-archive' => ['joda'], + 'application/vnd.kahootz' => ['ktz', 'ktr'], + 'application/vnd.kde.karbon' => ['karbon'], + 'application/vnd.kde.kchart' => ['chrt'], + 'application/vnd.kde.kformula' => ['kfo'], + 'application/vnd.kde.kivio' => ['flw'], + 'application/vnd.kde.kontour' => ['kon'], + 'application/vnd.kde.kpresenter' => ['kpr', 'kpt'], + 'application/vnd.kde.kspread' => ['ksp'], + 'application/vnd.kde.kword' => ['kwd', 'kwt'], + 'application/vnd.kenameaapp' => ['htke'], + 'application/vnd.kidspiration' => ['kia'], + 'application/vnd.kinar' => ['kne', 'knp'], + 'application/vnd.koan' => ['skp', 'skd', 'skt', 'skm'], + 'application/vnd.kodak-descriptor' => ['sse'], + 'application/vnd.las.las+xml' => ['lasxml'], + 'application/vnd.llamagraphics.life-balance.desktop' => ['lbd'], + 'application/vnd.llamagraphics.life-balance.exchange+xml' => ['lbe'], + 'application/vnd.lotus-1-2-3' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/vnd.lotus-approach' => ['apr'], + 'application/vnd.lotus-freelance' => ['pre'], + 'application/vnd.lotus-notes' => ['nsf'], + 'application/vnd.lotus-organizer' => ['org'], + 'application/vnd.lotus-screencam' => ['scm'], + 'application/vnd.lotus-wordpro' => ['lwp'], + 'application/vnd.macports.portpkg' => ['portpkg'], + 'application/vnd.mapbox-vector-tile' => ['mvt'], + 'application/vnd.mcd' => ['mcd'], + 'application/vnd.medcalcdata' => ['mc1'], + 'application/vnd.mediastation.cdkey' => ['cdkey'], + 'application/vnd.mfer' => ['mwf'], + 'application/vnd.mfmp' => ['mfm'], + 'application/vnd.micrografx.flo' => ['flo'], + 'application/vnd.micrografx.igx' => ['igx'], + 'application/vnd.mif' => ['mif'], + 'application/vnd.mobius.daf' => ['daf'], + 'application/vnd.mobius.dis' => ['dis'], + 'application/vnd.mobius.mbk' => ['mbk'], + 'application/vnd.mobius.mqy' => ['mqy'], + 'application/vnd.mobius.msl' => ['msl'], + 'application/vnd.mobius.plc' => ['plc'], + 'application/vnd.mobius.txf' => ['txf'], + 'application/vnd.mophun.application' => ['mpn'], + 'application/vnd.mophun.certificate' => ['mpc'], + 'application/vnd.mozilla.xul+xml' => ['xul'], + 'application/vnd.ms-access' => ['mdb'], + 'application/vnd.ms-artgalry' => ['cil'], + 'application/vnd.ms-asf' => ['asf'], + 'application/vnd.ms-cab-compressed' => ['cab'], + 'application/vnd.ms-excel' => ['xls', 'xlm', 'xla', 'xlc', 'xlt', 'xlw', 'xll', 'xld'], + 'application/vnd.ms-excel.addin.macroenabled.12' => ['xlam'], + 'application/vnd.ms-excel.sheet.binary.macroenabled.12' => ['xlsb'], + 'application/vnd.ms-excel.sheet.macroenabled.12' => ['xlsm'], + 'application/vnd.ms-excel.template.macroenabled.12' => ['xltm'], + 'application/vnd.ms-fontobject' => ['eot'], + 'application/vnd.ms-htmlhelp' => ['chm'], + 'application/vnd.ms-ims' => ['ims'], + 'application/vnd.ms-lrm' => ['lrm'], + 'application/vnd.ms-officetheme' => ['thmx'], + 'application/vnd.ms-outlook' => ['msg'], + 'application/vnd.ms-pki.seccat' => ['cat'], + 'application/vnd.ms-pki.stl' => ['stl'], + 'application/vnd.ms-powerpoint' => ['ppt', 'pps', 'pot', 'ppz'], + 'application/vnd.ms-powerpoint.addin.macroenabled.12' => ['ppam'], + 'application/vnd.ms-powerpoint.presentation.macroenabled.12' => ['pptm'], + 'application/vnd.ms-powerpoint.slide.macroenabled.12' => ['sldm'], + 'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => ['ppsm'], + 'application/vnd.ms-powerpoint.template.macroenabled.12' => ['potm'], + 'application/vnd.ms-project' => ['mpp', 'mpt'], + 'application/vnd.ms-publisher' => ['pub'], + 'application/vnd.ms-tnef' => ['tnef', 'tnf'], + 'application/vnd.ms-visio.drawing.macroenabled.main+xml' => ['vsdm'], + 'application/vnd.ms-visio.drawing.main+xml' => ['vsdx'], + 'application/vnd.ms-visio.stencil.macroenabled.main+xml' => ['vssm'], + 'application/vnd.ms-visio.stencil.main+xml' => ['vssx'], + 'application/vnd.ms-visio.template.macroenabled.main+xml' => ['vstm'], + 'application/vnd.ms-visio.template.main+xml' => ['vstx'], + 'application/vnd.ms-word' => ['doc'], + 'application/vnd.ms-word.document.macroenabled.12' => ['docm'], + 'application/vnd.ms-word.template.macroenabled.12' => ['dotm'], + 'application/vnd.ms-works' => ['wps', 'wks', 'wcm', 'wdb', 'xlr'], + 'application/vnd.ms-wpl' => ['wpl'], + 'application/vnd.ms-xpsdocument' => ['xps'], + 'application/vnd.msaccess' => ['mdb'], + 'application/vnd.mseq' => ['mseq'], + 'application/vnd.musician' => ['mus'], + 'application/vnd.muvee.style' => ['msty'], + 'application/vnd.mynfc' => ['taglet'], + 'application/vnd.neurolanguage.nlu' => ['nlu'], + 'application/vnd.nintendo.snes.rom' => ['sfc', 'smc'], + 'application/vnd.nitf' => ['ntf', 'nitf'], + 'application/vnd.noblenet-directory' => ['nnd'], + 'application/vnd.noblenet-sealer' => ['nns'], + 'application/vnd.noblenet-web' => ['nnw'], + 'application/vnd.nokia.n-gage.ac+xml' => ['ac'], + 'application/vnd.nokia.n-gage.data' => ['ngdat'], + 'application/vnd.nokia.n-gage.symbian.install' => ['n-gage'], + 'application/vnd.nokia.radio-preset' => ['rpst'], + 'application/vnd.nokia.radio-presets' => ['rpss'], + 'application/vnd.novadigm.edm' => ['edm'], + 'application/vnd.novadigm.edx' => ['edx'], + 'application/vnd.novadigm.ext' => ['ext'], + 'application/vnd.oasis.docbook+xml' => ['dbk', 'docbook'], + 'application/vnd.oasis.opendocument.chart' => ['odc'], + 'application/vnd.oasis.opendocument.chart-template' => ['otc'], + 'application/vnd.oasis.opendocument.database' => ['odb'], + 'application/vnd.oasis.opendocument.formula' => ['odf'], + 'application/vnd.oasis.opendocument.formula-template' => ['odft', 'otf'], + 'application/vnd.oasis.opendocument.graphics' => ['odg'], + 'application/vnd.oasis.opendocument.graphics-flat-xml' => ['fodg'], + 'application/vnd.oasis.opendocument.graphics-template' => ['otg'], + 'application/vnd.oasis.opendocument.image' => ['odi'], + 'application/vnd.oasis.opendocument.image-template' => ['oti'], + 'application/vnd.oasis.opendocument.presentation' => ['odp'], + 'application/vnd.oasis.opendocument.presentation-flat-xml' => ['fodp'], + 'application/vnd.oasis.opendocument.presentation-template' => ['otp'], + 'application/vnd.oasis.opendocument.spreadsheet' => ['ods'], + 'application/vnd.oasis.opendocument.spreadsheet-flat-xml' => ['fods'], + 'application/vnd.oasis.opendocument.spreadsheet-template' => ['ots'], + 'application/vnd.oasis.opendocument.text' => ['odt'], + 'application/vnd.oasis.opendocument.text-flat-xml' => ['fodt'], + 'application/vnd.oasis.opendocument.text-master' => ['odm'], + 'application/vnd.oasis.opendocument.text-template' => ['ott'], + 'application/vnd.oasis.opendocument.text-web' => ['oth'], + 'application/vnd.olpc-sugar' => ['xo'], + 'application/vnd.oma.dd2+xml' => ['dd2'], + 'application/vnd.openblox.game+xml' => ['obgx'], + 'application/vnd.openofficeorg.extension' => ['oxt'], + 'application/vnd.openstreetmap.data+xml' => ['osm'], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => ['pptx'], + 'application/vnd.openxmlformats-officedocument.presentationml.slide' => ['sldx'], + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => ['ppsx'], + 'application/vnd.openxmlformats-officedocument.presentationml.template' => ['potx'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => ['xltx'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['docx'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => ['dotx'], + 'application/vnd.osgeo.mapguide.package' => ['mgp'], + 'application/vnd.osgi.dp' => ['dp'], + 'application/vnd.osgi.subsystem' => ['esa'], + 'application/vnd.palm' => ['pdb', 'pqa', 'oprc', 'prc'], + 'application/vnd.pawaafile' => ['paw'], + 'application/vnd.pg.format' => ['str'], + 'application/vnd.pg.osasli' => ['ei6'], + 'application/vnd.picsel' => ['efif'], + 'application/vnd.pmi.widget' => ['wg'], + 'application/vnd.pocketlearn' => ['plf'], + 'application/vnd.powerbuilder6' => ['pbd'], + 'application/vnd.previewsystems.box' => ['box'], + 'application/vnd.proteus.magazine' => ['mgz'], + 'application/vnd.publishare-delta-tree' => ['qps'], + 'application/vnd.pvi.ptid1' => ['ptid'], + 'application/vnd.quark.quarkxpress' => ['qxd', 'qxt', 'qwd', 'qwt', 'qxl', 'qxb'], + 'application/vnd.rar' => ['rar'], + 'application/vnd.realvnc.bed' => ['bed'], + 'application/vnd.recordare.musicxml' => ['mxl'], + 'application/vnd.recordare.musicxml+xml' => ['musicxml'], + 'application/vnd.rig.cryptonote' => ['cryptonote'], + 'application/vnd.rim.cod' => ['cod'], + 'application/vnd.rn-realmedia' => ['rm', 'rmj', 'rmm', 'rms', 'rmx', 'rmvb'], + 'application/vnd.rn-realmedia-vbr' => ['rmvb', 'rm', 'rmj', 'rmm', 'rms', 'rmx'], + 'application/vnd.route66.link66+xml' => ['link66'], + 'application/vnd.sailingtracker.track' => ['st'], + 'application/vnd.sdp' => ['sdp'], + 'application/vnd.seemail' => ['see'], + 'application/vnd.sema' => ['sema'], + 'application/vnd.semd' => ['semd'], + 'application/vnd.semf' => ['semf'], + 'application/vnd.shana.informed.formdata' => ['ifm'], + 'application/vnd.shana.informed.formtemplate' => ['itp'], + 'application/vnd.shana.informed.interchange' => ['iif'], + 'application/vnd.shana.informed.package' => ['ipk'], + 'application/vnd.simtech-mindmapper' => ['twd', 'twds'], + 'application/vnd.smaf' => ['mmf', 'smaf'], + 'application/vnd.smart.teacher' => ['teacher'], + 'application/vnd.snap' => ['snap'], + 'application/vnd.software602.filler.form+xml' => ['fo'], + 'application/vnd.solent.sdkm+xml' => ['sdkm', 'sdkd'], + 'application/vnd.spotfire.dxp' => ['dxp'], + 'application/vnd.spotfire.sfs' => ['sfs'], + 'application/vnd.sqlite3' => ['sqlite3'], + 'application/vnd.squashfs' => ['sqsh'], + 'application/vnd.stardivision.calc' => ['sdc'], + 'application/vnd.stardivision.chart' => ['sds'], + 'application/vnd.stardivision.draw' => ['sda'], + 'application/vnd.stardivision.impress' => ['sdd', 'sdp'], + 'application/vnd.stardivision.mail' => ['smd'], + 'application/vnd.stardivision.math' => ['smf'], + 'application/vnd.stardivision.writer' => ['sdw', 'vor', 'sgl'], + 'application/vnd.stardivision.writer-global' => ['sgl', 'sdw', 'vor'], + 'application/vnd.stepmania.package' => ['smzip'], + 'application/vnd.stepmania.stepchart' => ['sm'], + 'application/vnd.sun.wadl+xml' => ['wadl'], + 'application/vnd.sun.xml.base' => ['odb'], + 'application/vnd.sun.xml.calc' => ['sxc'], + 'application/vnd.sun.xml.calc.template' => ['stc'], + 'application/vnd.sun.xml.draw' => ['sxd'], + 'application/vnd.sun.xml.draw.template' => ['std'], + 'application/vnd.sun.xml.impress' => ['sxi'], + 'application/vnd.sun.xml.impress.template' => ['sti'], + 'application/vnd.sun.xml.math' => ['sxm'], + 'application/vnd.sun.xml.writer' => ['sxw'], + 'application/vnd.sun.xml.writer.global' => ['sxg'], + 'application/vnd.sun.xml.writer.template' => ['stw'], + 'application/vnd.sus-calendar' => ['sus', 'susp'], + 'application/vnd.svd' => ['svd'], + 'application/vnd.symbian.install' => ['sis', 'sisx'], + 'application/vnd.syncml+xml' => ['xsm'], + 'application/vnd.syncml.dm+wbxml' => ['bdm'], + 'application/vnd.syncml.dm+xml' => ['xdm'], + 'application/vnd.syncml.dmddf+xml' => ['ddf'], + 'application/vnd.tao.intent-module-archive' => ['tao'], + 'application/vnd.tcpdump.pcap' => ['pcap', 'cap', 'dmp'], + 'application/vnd.tmobile-livetv' => ['tmo'], + 'application/vnd.trid.tpt' => ['tpt'], + 'application/vnd.triscape.mxs' => ['mxs'], + 'application/vnd.trueapp' => ['tra'], + 'application/vnd.ufdl' => ['ufd', 'ufdl'], + 'application/vnd.uiq.theme' => ['utz'], + 'application/vnd.umajin' => ['umj'], + 'application/vnd.unity' => ['unityweb'], + 'application/vnd.uoml+xml' => ['uoml'], + 'application/vnd.vcx' => ['vcx'], + 'application/vnd.visio' => ['vsd', 'vst', 'vss', 'vsw'], + 'application/vnd.visionary' => ['vis'], + 'application/vnd.vsf' => ['vsf'], + 'application/vnd.wap.wbxml' => ['wbxml'], + 'application/vnd.wap.wmlc' => ['wmlc'], + 'application/vnd.wap.wmlscriptc' => ['wmlsc'], + 'application/vnd.webturbo' => ['wtb'], + 'application/vnd.wolfram.player' => ['nbp'], + 'application/vnd.wordperfect' => ['wpd', 'wp', 'wp4', 'wp5', 'wp6', 'wpp'], + 'application/vnd.wqd' => ['wqd'], + 'application/vnd.wt.stf' => ['stf'], + 'application/vnd.xara' => ['xar'], + 'application/vnd.xdgapp' => ['flatpak', 'xdgapp'], + 'application/vnd.xfdl' => ['xfdl'], + 'application/vnd.yamaha.hv-dic' => ['hvd'], + 'application/vnd.yamaha.hv-script' => ['hvs'], + 'application/vnd.yamaha.hv-voice' => ['hvp'], + 'application/vnd.yamaha.openscoreformat' => ['osf'], + 'application/vnd.yamaha.openscoreformat.osfpvg+xml' => ['osfpvg'], + 'application/vnd.yamaha.smaf-audio' => ['saf'], + 'application/vnd.yamaha.smaf-phrase' => ['spf'], + 'application/vnd.yellowriver-custom-menu' => ['cmp'], + 'application/vnd.youtube.yt' => ['yt'], + 'application/vnd.zul' => ['zir', 'zirz'], + 'application/vnd.zzazz.deck+xml' => ['zaz'], + 'application/voicexml+xml' => ['vxml'], + 'application/wasm' => ['wasm'], + 'application/widget' => ['wgt'], + 'application/winhlp' => ['hlp'], + 'application/wk1' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/wmf' => ['wmf'], + 'application/wordperfect' => ['wp', 'wp4', 'wp5', 'wp6', 'wpd', 'wpp'], + 'application/wsdl+xml' => ['wsdl'], + 'application/wspolicy+xml' => ['wspolicy'], + 'application/wwf' => ['wwf'], + 'application/x-123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/x-7z-compressed' => ['7z', '7z.001'], + 'application/x-abiword' => ['abw', 'abw.CRASHED', 'abw.gz', 'zabw'], + 'application/x-ace' => ['ace'], + 'application/x-ace-compressed' => ['ace'], + 'application/x-alz' => ['alz'], + 'application/x-amiga-disk-format' => ['adf'], + 'application/x-amipro' => ['sam'], + 'application/x-annodex' => ['anx'], + 'application/x-aportisdoc' => ['pdb', 'pdc'], + 'application/x-apple-diskimage' => ['dmg'], + 'application/x-apple-systemprofiler+xml' => ['spx'], + 'application/x-appleworks-document' => ['cwk'], + 'application/x-applix-spreadsheet' => ['as'], + 'application/x-applix-word' => ['aw'], + 'application/x-archive' => ['a', 'ar'], + 'application/x-arj' => ['arj'], + 'application/x-asp' => ['asp'], + 'application/x-atari-2600-rom' => ['a26'], + 'application/x-atari-7800-rom' => ['a78'], + 'application/x-atari-lynx-rom' => ['lnx'], + 'application/x-authorware-bin' => ['aab', 'x32', 'u32', 'vox'], + 'application/x-authorware-map' => ['aam'], + 'application/x-authorware-seg' => ['aas'], + 'application/x-awk' => ['awk'], + 'application/x-bcpio' => ['bcpio'], + 'application/x-bdoc' => ['bdoc'], + 'application/x-bittorrent' => ['torrent'], + 'application/x-blender' => ['blender', 'blend', 'BLEND'], + 'application/x-blorb' => ['blb', 'blorb'], + 'application/x-bps-patch' => ['bps'], + 'application/x-bsdiff' => ['bsdiff'], + 'application/x-bz2' => ['bz2'], + 'application/x-bzdvi' => ['dvi.bz2'], + 'application/x-bzip' => ['bz', 'bz2'], + 'application/x-bzip-compressed-tar' => ['tar.bz2', 'tar.bz', 'tbz2', 'tbz', 'tb2'], + 'application/x-bzip2' => ['bz2', 'boz', 'bz'], + 'application/x-bzpdf' => ['pdf.bz2'], + 'application/x-bzpostscript' => ['ps.bz2'], + 'application/x-cb7' => ['cb7'], + 'application/x-cbr' => ['cbr', 'cba', 'cbt', 'cbz', 'cb7'], + 'application/x-cbt' => ['cbt'], + 'application/x-cbz' => ['cbz'], + 'application/x-ccmx' => ['ccmx'], + 'application/x-cd-image' => ['iso', 'iso9660'], + 'application/x-cdlink' => ['vcd'], + 'application/x-cdr' => ['cdr'], + 'application/x-cdrdao-toc' => ['toc'], + 'application/x-cfs-compressed' => ['cfs'], + 'application/x-chat' => ['chat'], + 'application/x-chess-pgn' => ['pgn'], + 'application/x-chm' => ['chm'], + 'application/x-chrome-extension' => ['crx'], + 'application/x-cisco-vpn-settings' => ['pcf'], + 'application/x-cocoa' => ['cco'], + 'application/x-compress' => ['Z'], + 'application/x-compressed-iso' => ['cso'], + 'application/x-compressed-tar' => ['tar.gz', 'tgz'], + 'application/x-conference' => ['nsc'], + 'application/x-coreldraw' => ['cdr'], + 'application/x-cpio' => ['cpio'], + 'application/x-cpio-compressed' => ['cpio.gz'], + 'application/x-csh' => ['csh'], + 'application/x-cue' => ['cue'], + 'application/x-dar' => ['dar'], + 'application/x-dbase' => ['dbf'], + 'application/x-dbf' => ['dbf'], + 'application/x-dc-rom' => ['dc'], + 'application/x-deb' => ['deb', 'udeb'], + 'application/x-debian-package' => ['deb', 'udeb'], + 'application/x-designer' => ['ui'], + 'application/x-desktop' => ['desktop', 'kdelnk'], + 'application/x-dgc-compressed' => ['dgc'], + 'application/x-dia-diagram' => ['dia'], + 'application/x-dia-shape' => ['shape'], + 'application/x-director' => ['dir', 'dcr', 'dxr', 'cst', 'cct', 'cxt', 'w3d', 'fgd', 'swa'], + 'application/x-discjuggler-cd-image' => ['cdi'], + 'application/x-docbook+xml' => ['dbk', 'docbook'], + 'application/x-doom' => ['wad'], + 'application/x-doom-wad' => ['wad'], + 'application/x-dreamcast-rom' => ['iso'], + 'application/x-dtbncx+xml' => ['ncx'], + 'application/x-dtbook+xml' => ['dtb'], + 'application/x-dtbresource+xml' => ['res'], + 'application/x-dvi' => ['dvi'], + 'application/x-e-theme' => ['etheme'], + 'application/x-egon' => ['egon'], + 'application/x-emf' => ['emf'], + 'application/x-envoy' => ['evy'], + 'application/x-eva' => ['eva'], + 'application/x-fd-file' => ['fd', 'qd'], + 'application/x-fds-disk' => ['fds'], + 'application/x-fictionbook' => ['fb2'], + 'application/x-fictionbook+xml' => ['fb2'], + 'application/x-flash-video' => ['flv'], + 'application/x-fluid' => ['fl'], + 'application/x-font-afm' => ['afm'], + 'application/x-font-bdf' => ['bdf'], + 'application/x-font-ghostscript' => ['gsf'], + 'application/x-font-linux-psf' => ['psf'], + 'application/x-font-otf' => ['otf'], + 'application/x-font-pcf' => ['pcf', 'pcf.Z', 'pcf.gz'], + 'application/x-font-snf' => ['snf'], + 'application/x-font-speedo' => ['spd'], + 'application/x-font-truetype' => ['ttf'], + 'application/x-font-ttf' => ['ttf'], + 'application/x-font-ttx' => ['ttx'], + 'application/x-font-type1' => ['pfa', 'pfb', 'pfm', 'afm', 'gsf'], + 'application/x-font-woff' => ['woff'], + 'application/x-frame' => ['fm'], + 'application/x-freearc' => ['arc'], + 'application/x-futuresplash' => ['spl'], + 'application/x-gameboy-color-rom' => ['gbc', 'cgb'], + 'application/x-gameboy-rom' => ['gb', 'sgb'], + 'application/x-gamecube-iso-image' => ['iso'], + 'application/x-gamecube-rom' => ['iso'], + 'application/x-gamegear-rom' => ['gg'], + 'application/x-gba-rom' => ['gba', 'agb'], + 'application/x-gca-compressed' => ['gca'], + 'application/x-gd-rom-cue' => ['gdi'], + 'application/x-gedcom' => ['ged', 'gedcom'], + 'application/x-genesis-32x-rom' => ['32x', 'mdx'], + 'application/x-genesis-rom' => ['gen', 'smd', 'sgd'], + 'application/x-gettext' => ['po'], + 'application/x-gettext-translation' => ['gmo', 'mo'], + 'application/x-glade' => ['glade'], + 'application/x-glulx' => ['ulx'], + 'application/x-gnome-app-info' => ['desktop', 'kdelnk'], + 'application/x-gnucash' => ['gnucash', 'gnc', 'xac'], + 'application/x-gnumeric' => ['gnumeric'], + 'application/x-gnuplot' => ['gp', 'gplt', 'gnuplot'], + 'application/x-go-sgf' => ['sgf'], + 'application/x-gpx' => ['gpx'], + 'application/x-gpx+xml' => ['gpx'], + 'application/x-gramps-xml' => ['gramps'], + 'application/x-graphite' => ['gra'], + 'application/x-gtar' => ['gtar', 'tar', 'gem'], + 'application/x-gtk-builder' => ['ui'], + 'application/x-gz-font-linux-psf' => ['psf.gz'], + 'application/x-gzdvi' => ['dvi.gz'], + 'application/x-gzip' => ['gz'], + 'application/x-gzpdf' => ['pdf.gz'], + 'application/x-gzpostscript' => ['ps.gz'], + 'application/x-hdf' => ['hdf', 'hdf4', 'h4', 'hdf5', 'h5'], + 'application/x-hfe-file' => ['hfe'], + 'application/x-hfe-floppy-image' => ['hfe'], + 'application/x-httpd-php' => ['php'], + 'application/x-hwp' => ['hwp'], + 'application/x-hwt' => ['hwt'], + 'application/x-ica' => ['ica'], + 'application/x-install-instructions' => ['install'], + 'application/x-ips-patch' => ['ips'], + 'application/x-ipynb+json' => ['ipynb'], + 'application/x-iso9660-appimage' => ['appimage'], + 'application/x-iso9660-image' => ['iso', 'iso9660'], + 'application/x-it87' => ['it87'], + 'application/x-iwork-keynote-sffkey' => ['key'], + 'application/x-iwork-numbers-sffnumbers' => ['numbers'], + 'application/x-iwork-pages-sffpages' => ['pages'], + 'application/x-jar' => ['jar'], + 'application/x-java' => ['class'], + 'application/x-java-archive' => ['jar'], + 'application/x-java-archive-diff' => ['jardiff'], + 'application/x-java-class' => ['class'], + 'application/x-java-jce-keystore' => ['jceks'], + 'application/x-java-jnlp-file' => ['jnlp'], + 'application/x-java-keystore' => ['jks', 'ks'], + 'application/x-java-pack200' => ['pack'], + 'application/x-java-vm' => ['class'], + 'application/x-javascript' => ['js', 'jsm', 'mjs'], + 'application/x-jbuilder-project' => ['jpr', 'jpx'], + 'application/x-karbon' => ['karbon'], + 'application/x-kchart' => ['chrt'], + 'application/x-keepass2' => ['kdbx'], + 'application/x-kexi-connectiondata' => ['kexic'], + 'application/x-kexiproject-shortcut' => ['kexis'], + 'application/x-kexiproject-sqlite' => ['kexi'], + 'application/x-kexiproject-sqlite2' => ['kexi'], + 'application/x-kexiproject-sqlite3' => ['kexi'], + 'application/x-kformula' => ['kfo'], + 'application/x-killustrator' => ['kil'], + 'application/x-kivio' => ['flw'], + 'application/x-kontour' => ['kon'], + 'application/x-kpovmodeler' => ['kpm'], + 'application/x-kpresenter' => ['kpr', 'kpt'], + 'application/x-krita' => ['kra', 'krz'], + 'application/x-kspread' => ['ksp'], + 'application/x-kugar' => ['kud'], + 'application/x-kword' => ['kwd', 'kwt'], + 'application/x-latex' => ['latex'], + 'application/x-lha' => ['lha', 'lzh'], + 'application/x-lhz' => ['lhz'], + 'application/x-linguist' => ['ts'], + 'application/x-lotus123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/x-lrzip' => ['lrz'], + 'application/x-lrzip-compressed-tar' => ['tar.lrz', 'tlrz'], + 'application/x-lua-bytecode' => ['luac'], + 'application/x-lyx' => ['lyx'], + 'application/x-lz4' => ['lz4'], + 'application/x-lz4-compressed-tar' => ['tar.lz4'], + 'application/x-lzh-compressed' => ['lzh', 'lha'], + 'application/x-lzip' => ['lz'], + 'application/x-lzip-compressed-tar' => ['tar.lz'], + 'application/x-lzma' => ['lzma'], + 'application/x-lzma-compressed-tar' => ['tar.lzma', 'tlz'], + 'application/x-lzop' => ['lzo'], + 'application/x-lzpdf' => ['pdf.lz'], + 'application/x-m4' => ['m4'], + 'application/x-magicpoint' => ['mgp'], + 'application/x-makeself' => ['run'], + 'application/x-mame-chd' => ['chd'], + 'application/x-markaby' => ['mab'], + 'application/x-mathematica' => ['nb'], + 'application/x-mdb' => ['mdb'], + 'application/x-mie' => ['mie'], + 'application/x-mif' => ['mif'], + 'application/x-mimearchive' => ['mhtml', 'mht'], + 'application/x-mobi8-ebook' => ['azw3', 'kfx'], + 'application/x-mobipocket-ebook' => ['prc', 'mobi'], + 'application/x-ms-application' => ['application'], + 'application/x-ms-asx' => ['asx', 'wax', 'wvx', 'wmx'], + 'application/x-ms-dos-executable' => ['exe'], + 'application/x-ms-shortcut' => ['lnk'], + 'application/x-ms-wim' => ['wim', 'swm'], + 'application/x-ms-wmd' => ['wmd'], + 'application/x-ms-wmz' => ['wmz'], + 'application/x-ms-xbap' => ['xbap'], + 'application/x-msaccess' => ['mdb'], + 'application/x-msbinder' => ['obd'], + 'application/x-mscardfile' => ['crd'], + 'application/x-msclip' => ['clp'], + 'application/x-msdos-program' => ['exe'], + 'application/x-msdownload' => ['exe', 'dll', 'com', 'bat', 'msi'], + 'application/x-msexcel' => ['xls', 'xlc', 'xll', 'xlm', 'xlw', 'xla', 'xlt', 'xld'], + 'application/x-msi' => ['msi'], + 'application/x-msmediaview' => ['mvb', 'm13', 'm14'], + 'application/x-msmetafile' => ['wmf', 'wmz', 'emf', 'emz'], + 'application/x-msmoney' => ['mny'], + 'application/x-mspowerpoint' => ['ppz', 'ppt', 'pps', 'pot'], + 'application/x-mspublisher' => ['pub'], + 'application/x-msschedule' => ['scd'], + 'application/x-msterminal' => ['trm'], + 'application/x-mswinurl' => ['url'], + 'application/x-msword' => ['doc'], + 'application/x-mswrite' => ['wri'], + 'application/x-msx-rom' => ['msx'], + 'application/x-n64-rom' => ['n64', 'z64', 'v64'], + 'application/x-navi-animation' => ['ani'], + 'application/x-neo-geo-pocket-color-rom' => ['ngc'], + 'application/x-neo-geo-pocket-rom' => ['ngp'], + 'application/x-nes-rom' => ['nes', 'nez', 'unf', 'unif'], + 'application/x-netcdf' => ['nc', 'cdf'], + 'application/x-netshow-channel' => ['nsc'], + 'application/x-nintendo-3ds-executable' => ['3dsx'], + 'application/x-nintendo-3ds-rom' => ['3ds', 'cci'], + 'application/x-nintendo-ds-rom' => ['nds'], + 'application/x-ns-proxy-autoconfig' => ['pac'], + 'application/x-nzb' => ['nzb'], + 'application/x-object' => ['o', 'mod'], + 'application/x-ogg' => ['ogx'], + 'application/x-oleo' => ['oleo'], + 'application/x-pagemaker' => ['p65', 'pm', 'pm6', 'pmd'], + 'application/x-pak' => ['pak'], + 'application/x-palm-database' => ['prc', 'pdb', 'pqa', 'oprc'], + 'application/x-par2' => ['PAR2', 'par2'], + 'application/x-partial-download' => ['wkdownload', 'crdownload', 'part'], + 'application/x-pc-engine-rom' => ['pce'], + 'application/x-pcap' => ['pcap', 'cap', 'dmp'], + 'application/x-pdf' => ['pdf'], + 'application/x-perl' => ['pl', 'pm', 'PL', 'al', 'perl', 'pod', 't'], + 'application/x-photoshop' => ['psd'], + 'application/x-php' => ['php', 'php3', 'php4', 'php5', 'phps'], + 'application/x-pilot' => ['prc', 'pdb'], + 'application/x-pkcs12' => ['p12', 'pfx'], + 'application/x-pkcs7-certificates' => ['p7b', 'spc'], + 'application/x-pkcs7-certreqresp' => ['p7r'], + 'application/x-planperfect' => ['pln'], + 'application/x-pocket-word' => ['psw'], + 'application/x-pw' => ['pw'], + 'application/x-pyspread-bz-spreadsheet' => ['pys'], + 'application/x-pyspread-spreadsheet' => ['pysu'], + 'application/x-python-bytecode' => ['pyc', 'pyo'], + 'application/x-qed-disk' => ['qed'], + 'application/x-qemu-disk' => ['qcow2', 'qcow'], + 'application/x-qpress' => ['qp'], + 'application/x-qtiplot' => ['qti', 'qti.gz'], + 'application/x-quattropro' => ['wb1', 'wb2', 'wb3'], + 'application/x-quicktime-media-link' => ['qtl'], + 'application/x-quicktimeplayer' => ['qtl'], + 'application/x-qw' => ['qif'], + 'application/x-rar' => ['rar'], + 'application/x-rar-compressed' => ['rar'], + 'application/x-raw-disk-image' => ['raw-disk-image', 'img'], + 'application/x-raw-disk-image-xz-compressed' => ['raw-disk-image.xz', 'img.xz'], + 'application/x-raw-floppy-disk-image' => ['fd', 'qd'], + 'application/x-redhat-package-manager' => ['rpm'], + 'application/x-reject' => ['rej'], + 'application/x-research-info-systems' => ['ris'], + 'application/x-rnc' => ['rnc'], + 'application/x-rpm' => ['rpm'], + 'application/x-ruby' => ['rb'], + 'application/x-sami' => ['smi', 'sami'], + 'application/x-sap-file' => ['sap'], + 'application/x-saturn-rom' => ['iso'], + 'application/x-sdp' => ['sdp'], + 'application/x-sea' => ['sea'], + 'application/x-sega-cd-rom' => ['iso'], + 'application/x-sega-pico-rom' => ['iso'], + 'application/x-sg1000-rom' => ['sg'], + 'application/x-sh' => ['sh'], + 'application/x-shar' => ['shar'], + 'application/x-shared-library-la' => ['la'], + 'application/x-sharedlib' => ['so'], + 'application/x-shellscript' => ['sh'], + 'application/x-shockwave-flash' => ['swf', 'spl'], + 'application/x-shorten' => ['shn'], + 'application/x-siag' => ['siag'], + 'application/x-silverlight-app' => ['xap'], + 'application/x-sit' => ['sit'], + 'application/x-smaf' => ['mmf', 'smaf'], + 'application/x-sms-rom' => ['sms'], + 'application/x-snes-rom' => ['sfc', 'smc'], + 'application/x-source-rpm' => ['src.rpm', 'spm'], + 'application/x-spss-por' => ['por'], + 'application/x-spss-sav' => ['sav', 'zsav'], + 'application/x-spss-savefile' => ['sav', 'zsav'], + 'application/x-sql' => ['sql'], + 'application/x-sqlite2' => ['sqlite2'], + 'application/x-sqlite3' => ['sqlite3'], + 'application/x-srt' => ['srt'], + 'application/x-stuffit' => ['sit'], + 'application/x-stuffitx' => ['sitx'], + 'application/x-subrip' => ['srt'], + 'application/x-sv4cpio' => ['sv4cpio'], + 'application/x-sv4crc' => ['sv4crc'], + 'application/x-t3vm-image' => ['t3'], + 'application/x-t602' => ['602'], + 'application/x-tads' => ['gam'], + 'application/x-tar' => ['tar', 'gtar', 'gem'], + 'application/x-targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'application/x-tarz' => ['tar.Z', 'taz'], + 'application/x-tcl' => ['tcl', 'tk'], + 'application/x-tex' => ['tex', 'ltx', 'sty', 'cls', 'dtx', 'ins', 'latex'], + 'application/x-tex-gf' => ['gf'], + 'application/x-tex-pk' => ['pk'], + 'application/x-tex-tfm' => ['tfm'], + 'application/x-texinfo' => ['texinfo', 'texi'], + 'application/x-tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'application/x-tgif' => ['obj'], + 'application/x-theme' => ['theme'], + 'application/x-thomson-cartridge-memo7' => ['m7'], + 'application/x-thomson-cassette' => ['k7'], + 'application/x-thomson-sap-image' => ['sap'], + 'application/x-trash' => ['bak', 'old', 'sik'], + 'application/x-trig' => ['trig'], + 'application/x-troff' => ['tr', 'roff', 't'], + 'application/x-troff-man' => ['man'], + 'application/x-tzo' => ['tar.lzo', 'tzo'], + 'application/x-ufraw' => ['ufraw'], + 'application/x-ustar' => ['ustar'], + 'application/x-vdi-disk' => ['vdi'], + 'application/x-vhd-disk' => ['vhd', 'vpc'], + 'application/x-vhdx-disk' => ['vhdx'], + 'application/x-virtual-boy-rom' => ['vb'], + 'application/x-virtualbox-hdd' => ['hdd'], + 'application/x-virtualbox-ova' => ['ova'], + 'application/x-virtualbox-ovf' => ['ovf'], + 'application/x-virtualbox-vbox' => ['vbox'], + 'application/x-virtualbox-vbox-extpack' => ['vbox-extpack'], + 'application/x-virtualbox-vdi' => ['vdi'], + 'application/x-virtualbox-vhd' => ['vhd', 'vpc'], + 'application/x-virtualbox-vhdx' => ['vhdx'], + 'application/x-virtualbox-vmdk' => ['vmdk'], + 'application/x-vmdk-disk' => ['vmdk'], + 'application/x-vnd.kde.kexi' => ['kexi'], + 'application/x-wais-source' => ['src'], + 'application/x-wbfs' => ['iso'], + 'application/x-web-app-manifest+json' => ['webapp'], + 'application/x-wia' => ['iso'], + 'application/x-wii-iso-image' => ['iso'], + 'application/x-wii-rom' => ['iso'], + 'application/x-wii-wad' => ['wad'], + 'application/x-windows-themepack' => ['themepack'], + 'application/x-wmf' => ['wmf'], + 'application/x-wonderswan-color-rom' => ['wsc'], + 'application/x-wonderswan-rom' => ['ws'], + 'application/x-wordperfect' => ['wp', 'wp4', 'wp5', 'wp6', 'wpd', 'wpp'], + 'application/x-wpg' => ['wpg'], + 'application/x-wwf' => ['wwf'], + 'application/x-x509-ca-cert' => ['der', 'crt', 'pem', 'cert'], + 'application/x-xar' => ['xar', 'pkg'], + 'application/x-xbel' => ['xbel'], + 'application/x-xfig' => ['fig'], + 'application/x-xliff' => ['xlf', 'xliff'], + 'application/x-xliff+xml' => ['xlf'], + 'application/x-xpinstall' => ['xpi'], + 'application/x-xspf+xml' => ['xspf'], + 'application/x-xz' => ['xz'], + 'application/x-xz-compressed-tar' => ['tar.xz', 'txz'], + 'application/x-xzpdf' => ['pdf.xz'], + 'application/x-yaml' => ['yaml', 'yml'], + 'application/x-zip' => ['zip'], + 'application/x-zip-compressed' => ['zip'], + 'application/x-zip-compressed-fb2' => ['fb2.zip'], + 'application/x-zmachine' => ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'], + 'application/x-zoo' => ['zoo'], + 'application/x-zstd-compressed-tar' => ['tar.zst', 'tzst'], + 'application/xaml+xml' => ['xaml'], + 'application/xcap-att+xml' => ['xav'], + 'application/xcap-caps+xml' => ['xca'], + 'application/xcap-diff+xml' => ['xdf'], + 'application/xcap-el+xml' => ['xel'], + 'application/xcap-error+xml' => ['xer'], + 'application/xcap-ns+xml' => ['xns'], + 'application/xenc+xml' => ['xenc'], + 'application/xhtml+xml' => ['xhtml', 'xht', 'html', 'htm'], + 'application/xliff+xml' => ['xlf', 'xliff'], + 'application/xml' => ['xml', 'xsl', 'xsd', 'rng', 'xbl'], + 'application/xml-dtd' => ['dtd'], + 'application/xml-external-parsed-entity' => ['ent'], + 'application/xop+xml' => ['xop'], + 'application/xproc+xml' => ['xpl'], + 'application/xps' => ['xps'], + 'application/xslt+xml' => ['xsl', 'xslt'], + 'application/xspf+xml' => ['xspf'], + 'application/xv+xml' => ['mxml', 'xhvml', 'xvml', 'xvm'], + 'application/yang' => ['yang'], + 'application/yin+xml' => ['yin'], + 'application/zip' => ['zip'], + 'application/zlib' => ['zz'], + 'application/zstd' => ['zst'], + 'audio/3gpp' => ['3gpp', '3gp', '3ga'], + 'audio/3gpp-encrypted' => ['3gp', '3gpp', '3ga'], + 'audio/3gpp2' => ['3g2', '3gp2', '3gpp2'], + 'audio/aac' => ['aac', 'adts', 'ass'], + 'audio/ac3' => ['ac3'], + 'audio/adpcm' => ['adp'], + 'audio/amr' => ['amr'], + 'audio/amr-encrypted' => ['amr'], + 'audio/amr-wb' => ['awb'], + 'audio/amr-wb-encrypted' => ['awb'], + 'audio/annodex' => ['axa'], + 'audio/basic' => ['au', 'snd'], + 'audio/flac' => ['flac'], + 'audio/imelody' => ['imy', 'ime'], + 'audio/m3u' => ['m3u', 'm3u8', 'vlc'], + 'audio/m4a' => ['m4a', 'f4a'], + 'audio/midi' => ['mid', 'midi', 'kar', 'rmi'], + 'audio/mobile-xmf' => ['mxmf', 'xmf'], + 'audio/mp2' => ['mp2'], + 'audio/mp3' => ['mp3', 'mpga'], + 'audio/mp4' => ['m4a', 'mp4a', 'f4a'], + 'audio/mpeg' => ['mp3', 'mpga', 'mp2', 'mp2a', 'm2a', 'm3a'], + 'audio/mpegurl' => ['m3u', 'm3u8', 'vlc'], + 'audio/ogg' => ['ogg', 'oga', 'spx', 'opus'], + 'audio/prs.sid' => ['sid', 'psid'], + 'audio/s3m' => ['s3m'], + 'audio/scpls' => ['pls'], + 'audio/silk' => ['sil'], + 'audio/tta' => ['tta'], + 'audio/usac' => ['loas', 'xhe'], + 'audio/vnd.audible' => ['aa', 'aax'], + 'audio/vnd.audible.aax' => ['aax'], + 'audio/vnd.dece.audio' => ['uva', 'uvva'], + 'audio/vnd.digital-winds' => ['eol'], + 'audio/vnd.dra' => ['dra'], + 'audio/vnd.dts' => ['dts'], + 'audio/vnd.dts.hd' => ['dtshd'], + 'audio/vnd.lucent.voice' => ['lvp'], + 'audio/vnd.m-realaudio' => ['ra', 'rax'], + 'audio/vnd.ms-playready.media.pya' => ['pya'], + 'audio/vnd.nuera.ecelp4800' => ['ecelp4800'], + 'audio/vnd.nuera.ecelp7470' => ['ecelp7470'], + 'audio/vnd.nuera.ecelp9600' => ['ecelp9600'], + 'audio/vnd.rip' => ['rip'], + 'audio/vnd.rn-realaudio' => ['ra', 'rax'], + 'audio/vnd.wave' => ['wav'], + 'audio/vorbis' => ['oga', 'ogg'], + 'audio/wav' => ['wav'], + 'audio/wave' => ['wav'], + 'audio/webm' => ['weba'], + 'audio/wma' => ['wma'], + 'audio/x-aac' => ['aac', 'adts', 'ass'], + 'audio/x-aifc' => ['aifc', 'aiffc'], + 'audio/x-aiff' => ['aif', 'aiff', 'aifc'], + 'audio/x-aiffc' => ['aifc', 'aiffc'], + 'audio/x-amzxml' => ['amz'], + 'audio/x-annodex' => ['axa'], + 'audio/x-ape' => ['ape'], + 'audio/x-caf' => ['caf'], + 'audio/x-dts' => ['dts'], + 'audio/x-dtshd' => ['dtshd'], + 'audio/x-flac' => ['flac'], + 'audio/x-flac+ogg' => ['oga', 'ogg'], + 'audio/x-gsm' => ['gsm'], + 'audio/x-hx-aac-adts' => ['aac', 'adts', 'ass'], + 'audio/x-imelody' => ['imy', 'ime'], + 'audio/x-iriver-pla' => ['pla'], + 'audio/x-it' => ['it'], + 'audio/x-m3u' => ['m3u', 'm3u8', 'vlc'], + 'audio/x-m4a' => ['m4a', 'f4a'], + 'audio/x-m4b' => ['m4b', 'f4b'], + 'audio/x-m4r' => ['m4r'], + 'audio/x-matroska' => ['mka'], + 'audio/x-midi' => ['mid', 'midi', 'kar'], + 'audio/x-minipsf' => ['minipsf'], + 'audio/x-mo3' => ['mo3'], + 'audio/x-mod' => ['mod', 'ult', 'uni', 'm15', 'mtm', '669', 'med'], + 'audio/x-mp2' => ['mp2'], + 'audio/x-mp3' => ['mp3', 'mpga'], + 'audio/x-mp3-playlist' => ['m3u', 'm3u8', 'vlc'], + 'audio/x-mpeg' => ['mp3', 'mpga'], + 'audio/x-mpegurl' => ['m3u', 'm3u8', 'vlc'], + 'audio/x-mpg' => ['mp3', 'mpga'], + 'audio/x-ms-asx' => ['asx', 'wax', 'wvx', 'wmx'], + 'audio/x-ms-wax' => ['wax'], + 'audio/x-ms-wma' => ['wma'], + 'audio/x-ms-wmv' => ['wmv'], + 'audio/x-musepack' => ['mpc', 'mpp', 'mp+'], + 'audio/x-ogg' => ['oga', 'ogg', 'opus'], + 'audio/x-oggflac' => ['oga', 'ogg'], + 'audio/x-opus+ogg' => ['opus'], + 'audio/x-pn-audibleaudio' => ['aa', 'aax'], + 'audio/x-pn-realaudio' => ['ram', 'ra', 'rax'], + 'audio/x-pn-realaudio-plugin' => ['rmp'], + 'audio/x-psf' => ['psf'], + 'audio/x-psflib' => ['psflib'], + 'audio/x-realaudio' => ['ra'], + 'audio/x-rn-3gpp-amr' => ['3gp', '3gpp', '3ga'], + 'audio/x-rn-3gpp-amr-encrypted' => ['3gp', '3gpp', '3ga'], + 'audio/x-rn-3gpp-amr-wb' => ['3gp', '3gpp', '3ga'], + 'audio/x-rn-3gpp-amr-wb-encrypted' => ['3gp', '3gpp', '3ga'], + 'audio/x-s3m' => ['s3m'], + 'audio/x-scpls' => ['pls'], + 'audio/x-shorten' => ['shn'], + 'audio/x-speex' => ['spx'], + 'audio/x-speex+ogg' => ['oga', 'ogg', 'spx'], + 'audio/x-stm' => ['stm'], + 'audio/x-tta' => ['tta'], + 'audio/x-voc' => ['voc'], + 'audio/x-vorbis' => ['oga', 'ogg'], + 'audio/x-vorbis+ogg' => ['oga', 'ogg'], + 'audio/x-wav' => ['wav'], + 'audio/x-wavpack' => ['wv', 'wvp'], + 'audio/x-wavpack-correction' => ['wvc'], + 'audio/x-xi' => ['xi'], + 'audio/x-xm' => ['xm'], + 'audio/x-xmf' => ['xmf'], + 'audio/xm' => ['xm'], + 'audio/xmf' => ['xmf'], + 'chemical/x-cdx' => ['cdx'], + 'chemical/x-cif' => ['cif'], + 'chemical/x-cmdf' => ['cmdf'], + 'chemical/x-cml' => ['cml'], + 'chemical/x-csml' => ['csml'], + 'chemical/x-xyz' => ['xyz'], + 'flv-application/octet-stream' => ['flv'], + 'font/collection' => ['ttc'], + 'font/otf' => ['otf'], + 'font/ttf' => ['ttf'], + 'font/woff' => ['woff'], + 'font/woff2' => ['woff2'], + 'image/aces' => ['exr'], + 'image/apng' => ['apng'], + 'image/astc' => ['astc'], + 'image/avif' => ['avif', 'avifs'], + 'image/avif-sequence' => ['avif', 'avifs'], + 'image/bmp' => ['bmp', 'dib'], + 'image/cdr' => ['cdr'], + 'image/cgm' => ['cgm'], + 'image/dicom-rle' => ['drle'], + 'image/emf' => ['emf'], + 'image/fax-g3' => ['g3'], + 'image/fits' => ['fits'], + 'image/g3fax' => ['g3'], + 'image/gif' => ['gif'], + 'image/heic' => ['heic', 'heif'], + 'image/heic-sequence' => ['heics', 'heic', 'heif'], + 'image/heif' => ['heif', 'heic'], + 'image/heif-sequence' => ['heifs', 'heic', 'heif'], + 'image/hej2k' => ['hej2'], + 'image/hsj2' => ['hsj2'], + 'image/ico' => ['ico'], + 'image/icon' => ['ico'], + 'image/ief' => ['ief'], + 'image/jls' => ['jls'], + 'image/jp2' => ['jp2', 'jpg2'], + 'image/jpeg' => ['jpg', 'jpeg', 'jpe'], + 'image/jpeg2000' => ['jp2', 'jpg2'], + 'image/jpeg2000-image' => ['jp2', 'jpg2'], + 'image/jph' => ['jph'], + 'image/jphc' => ['jhc'], + 'image/jpm' => ['jpm', 'jpgm'], + 'image/jpx' => ['jpx', 'jpf'], + 'image/jxl' => ['jxl'], + 'image/jxr' => ['jxr'], + 'image/jxra' => ['jxra'], + 'image/jxrs' => ['jxrs'], + 'image/jxs' => ['jxs'], + 'image/jxsc' => ['jxsc'], + 'image/jxsi' => ['jxsi'], + 'image/jxss' => ['jxss'], + 'image/ktx' => ['ktx'], + 'image/ktx2' => ['ktx2'], + 'image/openraster' => ['ora'], + 'image/pdf' => ['pdf'], + 'image/photoshop' => ['psd'], + 'image/pjpeg' => ['jpg', 'jpeg', 'jpe'], + 'image/png' => ['png'], + 'image/prs.btif' => ['btif'], + 'image/prs.pti' => ['pti'], + 'image/psd' => ['psd'], + 'image/rle' => ['rle'], + 'image/sgi' => ['sgi'], + 'image/svg' => ['svg'], + 'image/svg+xml' => ['svg', 'svgz'], + 'image/svg+xml-compressed' => ['svgz'], + 'image/t38' => ['t38'], + 'image/targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/tiff' => ['tif', 'tiff'], + 'image/tiff-fx' => ['tfx'], + 'image/vnd.adobe.photoshop' => ['psd'], + 'image/vnd.airzip.accelerator.azv' => ['azv'], + 'image/vnd.dece.graphic' => ['uvi', 'uvvi', 'uvg', 'uvvg'], + 'image/vnd.djvu' => ['djvu', 'djv'], + 'image/vnd.djvu+multipage' => ['djvu', 'djv'], + 'image/vnd.dvb.subtitle' => ['sub'], + 'image/vnd.dwg' => ['dwg'], + 'image/vnd.dxf' => ['dxf'], + 'image/vnd.fastbidsheet' => ['fbs'], + 'image/vnd.fpx' => ['fpx'], + 'image/vnd.fst' => ['fst'], + 'image/vnd.fujixerox.edmics-mmr' => ['mmr'], + 'image/vnd.fujixerox.edmics-rlc' => ['rlc'], + 'image/vnd.microsoft.icon' => ['ico'], + 'image/vnd.ms-dds' => ['dds'], + 'image/vnd.ms-modi' => ['mdi'], + 'image/vnd.ms-photo' => ['wdp'], + 'image/vnd.net-fpx' => ['npx'], + 'image/vnd.pco.b16' => ['b16'], + 'image/vnd.rn-realpix' => ['rp'], + 'image/vnd.tencent.tap' => ['tap'], + 'image/vnd.valve.source.texture' => ['vtf'], + 'image/vnd.wap.wbmp' => ['wbmp'], + 'image/vnd.xiff' => ['xif'], + 'image/vnd.zbrush.pcx' => ['pcx'], + 'image/webp' => ['webp'], + 'image/wmf' => ['wmf'], + 'image/x-3ds' => ['3ds'], + 'image/x-adobe-dng' => ['dng'], + 'image/x-applix-graphics' => ['ag'], + 'image/x-bmp' => ['bmp', 'dib'], + 'image/x-bzeps' => ['eps.bz2', 'epsi.bz2', 'epsf.bz2'], + 'image/x-canon-cr2' => ['cr2'], + 'image/x-canon-cr3' => ['cr3'], + 'image/x-canon-crw' => ['crw'], + 'image/x-cdr' => ['cdr'], + 'image/x-cmu-raster' => ['ras'], + 'image/x-cmx' => ['cmx'], + 'image/x-compressed-xcf' => ['xcf.gz', 'xcf.bz2'], + 'image/x-dds' => ['dds'], + 'image/x-djvu' => ['djvu', 'djv'], + 'image/x-emf' => ['emf'], + 'image/x-eps' => ['eps', 'epsi', 'epsf'], + 'image/x-exr' => ['exr'], + 'image/x-fits' => ['fits'], + 'image/x-freehand' => ['fh', 'fhc', 'fh4', 'fh5', 'fh7'], + 'image/x-fuji-raf' => ['raf'], + 'image/x-gimp-gbr' => ['gbr'], + 'image/x-gimp-gih' => ['gih'], + 'image/x-gimp-pat' => ['pat'], + 'image/x-gzeps' => ['eps.gz', 'epsi.gz', 'epsf.gz'], + 'image/x-icb' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/x-icns' => ['icns'], + 'image/x-ico' => ['ico'], + 'image/x-icon' => ['ico'], + 'image/x-iff' => ['iff', 'ilbm', 'lbm'], + 'image/x-ilbm' => ['iff', 'ilbm', 'lbm'], + 'image/x-jng' => ['jng'], + 'image/x-jp2-codestream' => ['j2c', 'j2k', 'jpc'], + 'image/x-jpeg2000-image' => ['jp2', 'jpg2'], + 'image/x-kodak-dcr' => ['dcr'], + 'image/x-kodak-k25' => ['k25'], + 'image/x-kodak-kdc' => ['kdc'], + 'image/x-lwo' => ['lwo', 'lwob'], + 'image/x-lws' => ['lws'], + 'image/x-macpaint' => ['pntg'], + 'image/x-minolta-mrw' => ['mrw'], + 'image/x-mrsid-image' => ['sid'], + 'image/x-ms-bmp' => ['bmp', 'dib'], + 'image/x-msod' => ['msod'], + 'image/x-nikon-nef' => ['nef'], + 'image/x-nikon-nrw' => ['nrw'], + 'image/x-olympus-orf' => ['orf'], + 'image/x-panasonic-raw' => ['raw'], + 'image/x-panasonic-raw2' => ['rw2'], + 'image/x-panasonic-rw' => ['raw'], + 'image/x-panasonic-rw2' => ['rw2'], + 'image/x-pcx' => ['pcx'], + 'image/x-pentax-pef' => ['pef'], + 'image/x-photo-cd' => ['pcd'], + 'image/x-photoshop' => ['psd'], + 'image/x-pict' => ['pic', 'pct', 'pict', 'pict1', 'pict2'], + 'image/x-portable-anymap' => ['pnm'], + 'image/x-portable-bitmap' => ['pbm'], + 'image/x-portable-graymap' => ['pgm'], + 'image/x-portable-pixmap' => ['ppm'], + 'image/x-psd' => ['psd'], + 'image/x-quicktime' => ['qtif', 'qif'], + 'image/x-rgb' => ['rgb'], + 'image/x-sgi' => ['sgi'], + 'image/x-sigma-x3f' => ['x3f'], + 'image/x-skencil' => ['sk', 'sk1'], + 'image/x-sony-arw' => ['arw'], + 'image/x-sony-sr2' => ['sr2'], + 'image/x-sony-srf' => ['srf'], + 'image/x-sun-raster' => ['sun'], + 'image/x-targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/x-tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/x-win-bitmap' => ['cur'], + 'image/x-win-metafile' => ['wmf'], + 'image/x-wmf' => ['wmf'], + 'image/x-xbitmap' => ['xbm'], + 'image/x-xcf' => ['xcf'], + 'image/x-xfig' => ['fig'], + 'image/x-xpixmap' => ['xpm'], + 'image/x-xpm' => ['xpm'], + 'image/x-xwindowdump' => ['xwd'], + 'image/x.djvu' => ['djvu', 'djv'], + 'message/disposition-notification' => ['disposition-notification'], + 'message/global' => ['u8msg'], + 'message/global-delivery-status' => ['u8dsn'], + 'message/global-disposition-notification' => ['u8mdn'], + 'message/global-headers' => ['u8hdr'], + 'message/rfc822' => ['eml', 'mime'], + 'message/vnd.wfa.wsc' => ['wsc'], + 'model/3mf' => ['3mf'], + 'model/gltf+json' => ['gltf'], + 'model/gltf-binary' => ['glb'], + 'model/iges' => ['igs', 'iges'], + 'model/mesh' => ['msh', 'mesh', 'silo'], + 'model/mtl' => ['mtl'], + 'model/obj' => ['obj'], + 'model/step+zip' => ['stpz'], + 'model/step-xml+zip' => ['stpxz'], + 'model/stl' => ['stl'], + 'model/vnd.collada+xml' => ['dae'], + 'model/vnd.dwf' => ['dwf'], + 'model/vnd.gdl' => ['gdl'], + 'model/vnd.gtw' => ['gtw'], + 'model/vnd.mts' => ['mts'], + 'model/vnd.opengex' => ['ogex'], + 'model/vnd.parasolid.transmit.binary' => ['x_b'], + 'model/vnd.parasolid.transmit.text' => ['x_t'], + 'model/vnd.sap.vds' => ['vds'], + 'model/vnd.usdz+zip' => ['usdz'], + 'model/vnd.valve.source.compiled-map' => ['bsp'], + 'model/vnd.vtu' => ['vtu'], + 'model/vrml' => ['wrl', 'vrml', 'vrm'], + 'model/x.stl-ascii' => ['stl'], + 'model/x.stl-binary' => ['stl'], + 'model/x3d+binary' => ['x3db', 'x3dbz'], + 'model/x3d+fastinfoset' => ['x3db'], + 'model/x3d+vrml' => ['x3dv', 'x3dvz'], + 'model/x3d+xml' => ['x3d', 'x3dz'], + 'model/x3d-vrml' => ['x3dv'], + 'text/cache-manifest' => ['appcache', 'manifest'], + 'text/calendar' => ['ics', 'ifb', 'vcs'], + 'text/coffeescript' => ['coffee', 'litcoffee'], + 'text/crystal' => ['cr'], + 'text/css' => ['css'], + 'text/csv' => ['csv'], + 'text/csv-schema' => ['csvs'], + 'text/directory' => ['vcard', 'vcf', 'vct', 'gcrd'], + 'text/ecmascript' => ['es'], + 'text/gedcom' => ['ged', 'gedcom'], + 'text/google-video-pointer' => ['gvp'], + 'text/html' => ['html', 'htm', 'shtml'], + 'text/ico' => ['ico'], + 'text/jade' => ['jade'], + 'text/javascript' => ['js', 'jsm', 'mjs'], + 'text/jsx' => ['jsx'], + 'text/less' => ['less'], + 'text/markdown' => ['md', 'markdown', 'mkd'], + 'text/mathml' => ['mml'], + 'text/mdx' => ['mdx'], + 'text/n3' => ['n3'], + 'text/org' => ['org'], + 'text/plain' => ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini', 'asc'], + 'text/prs.lines.tag' => ['dsc'], + 'text/rdf' => ['rdf', 'rdfs', 'owl'], + 'text/richtext' => ['rtx'], + 'text/rss' => ['rss'], + 'text/rtf' => ['rtf'], + 'text/rust' => ['rs'], + 'text/sgml' => ['sgml', 'sgm'], + 'text/shex' => ['shex'], + 'text/slim' => ['slim', 'slm'], + 'text/spdx' => ['spdx'], + 'text/spreadsheet' => ['sylk', 'slk'], + 'text/stylus' => ['stylus', 'styl'], + 'text/tab-separated-values' => ['tsv'], + 'text/tcl' => ['tcl', 'tk'], + 'text/troff' => ['t', 'tr', 'roff', 'man', 'me', 'ms'], + 'text/turtle' => ['ttl'], + 'text/uri-list' => ['uri', 'uris', 'urls'], + 'text/vbs' => ['vbs'], + 'text/vbscript' => ['vbs'], + 'text/vcard' => ['vcard', 'vcf', 'vct', 'gcrd'], + 'text/vnd.curl' => ['curl'], + 'text/vnd.curl.dcurl' => ['dcurl'], + 'text/vnd.curl.mcurl' => ['mcurl'], + 'text/vnd.curl.scurl' => ['scurl'], + 'text/vnd.dvb.subtitle' => ['sub'], + 'text/vnd.fly' => ['fly'], + 'text/vnd.fmi.flexstor' => ['flx'], + 'text/vnd.graphviz' => ['gv', 'dot'], + 'text/vnd.in3d.3dml' => ['3dml'], + 'text/vnd.in3d.spot' => ['spot'], + 'text/vnd.qt.linguist' => ['ts'], + 'text/vnd.rn-realtext' => ['rt'], + 'text/vnd.senx.warpscript' => ['mc2'], + 'text/vnd.sun.j2me.app-descriptor' => ['jad'], + 'text/vnd.trolltech.linguist' => ['ts'], + 'text/vnd.wap.wml' => ['wml'], + 'text/vnd.wap.wmlscript' => ['wmls'], + 'text/vtt' => ['vtt'], + 'text/x-adasrc' => ['adb', 'ads'], + 'text/x-asm' => ['s', 'asm'], + 'text/x-bibtex' => ['bib'], + 'text/x-c' => ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'], + 'text/x-c++hdr' => ['hh', 'hp', 'hpp', 'h++', 'hxx'], + 'text/x-c++src' => ['cpp', 'cxx', 'cc', 'C', 'c++'], + 'text/x-chdr' => ['h'], + 'text/x-cmake' => ['cmake'], + 'text/x-cobol' => ['cbl', 'cob'], + 'text/x-comma-separated-values' => ['csv'], + 'text/x-common-lisp' => ['asd', 'fasl', 'lisp', 'ros'], + 'text/x-component' => ['htc'], + 'text/x-crystal' => ['cr'], + 'text/x-csharp' => ['cs'], + 'text/x-csrc' => ['c'], + 'text/x-csv' => ['csv'], + 'text/x-dart' => ['dart'], + 'text/x-dbus-service' => ['service'], + 'text/x-dcl' => ['dcl'], + 'text/x-diff' => ['diff', 'patch'], + 'text/x-dsl' => ['dsl'], + 'text/x-dsrc' => ['d', 'di'], + 'text/x-dtd' => ['dtd'], + 'text/x-eiffel' => ['e', 'eif'], + 'text/x-elixir' => ['ex', 'exs'], + 'text/x-emacs-lisp' => ['el'], + 'text/x-erlang' => ['erl'], + 'text/x-fortran' => ['f', 'for', 'f77', 'f90', 'f95'], + 'text/x-genie' => ['gs'], + 'text/x-gettext-translation' => ['po'], + 'text/x-gettext-translation-template' => ['pot'], + 'text/x-gherkin' => ['feature'], + 'text/x-go' => ['go'], + 'text/x-google-video-pointer' => ['gvp'], + 'text/x-gradle' => ['gradle'], + 'text/x-groovy' => ['groovy', 'gvy', 'gy', 'gsh'], + 'text/x-handlebars-template' => ['hbs'], + 'text/x-haskell' => ['hs'], + 'text/x-idl' => ['idl'], + 'text/x-imelody' => ['imy', 'ime'], + 'text/x-iptables' => ['iptables'], + 'text/x-java' => ['java'], + 'text/x-java-source' => ['java'], + 'text/x-kaitai-struct' => ['ksy'], + 'text/x-kotlin' => ['kt'], + 'text/x-ldif' => ['ldif'], + 'text/x-lilypond' => ['ly'], + 'text/x-literate-haskell' => ['lhs'], + 'text/x-log' => ['log'], + 'text/x-lua' => ['lua'], + 'text/x-lyx' => ['lyx'], + 'text/x-makefile' => ['mk', 'mak'], + 'text/x-markdown' => ['md', 'mkd', 'markdown'], + 'text/x-matlab' => ['m'], + 'text/x-microdvd' => ['sub'], + 'text/x-moc' => ['moc'], + 'text/x-modelica' => ['mo'], + 'text/x-mof' => ['mof'], + 'text/x-mpsub' => ['sub'], + 'text/x-mrml' => ['mrml', 'mrl'], + 'text/x-ms-regedit' => ['reg'], + 'text/x-mup' => ['mup', 'not'], + 'text/x-nfo' => ['nfo'], + 'text/x-objcsrc' => ['m'], + 'text/x-ocaml' => ['ml', 'mli'], + 'text/x-ocl' => ['ocl'], + 'text/x-octave' => ['m'], + 'text/x-ooc' => ['ooc'], + 'text/x-opencl-src' => ['cl'], + 'text/x-opml' => ['opml'], + 'text/x-opml+xml' => ['opml'], + 'text/x-org' => ['org'], + 'text/x-pascal' => ['p', 'pas'], + 'text/x-patch' => ['diff', 'patch'], + 'text/x-perl' => ['pl', 'PL', 'pm', 'al', 'perl', 'pod', 't'], + 'text/x-po' => ['po'], + 'text/x-pot' => ['pot'], + 'text/x-processing' => ['pde'], + 'text/x-python' => ['py', 'pyx', 'wsgi'], + 'text/x-python3' => ['py', 'py3', 'py3x', 'pyi'], + 'text/x-qml' => ['qml', 'qmltypes', 'qmlproject'], + 'text/x-reject' => ['rej'], + 'text/x-rpm-spec' => ['spec'], + 'text/x-rst' => ['rst'], + 'text/x-sagemath' => ['sage'], + 'text/x-sass' => ['sass'], + 'text/x-scala' => ['scala', 'sc'], + 'text/x-scheme' => ['scm', 'ss'], + 'text/x-scss' => ['scss'], + 'text/x-setext' => ['etx'], + 'text/x-sfv' => ['sfv'], + 'text/x-sh' => ['sh'], + 'text/x-sql' => ['sql'], + 'text/x-ssa' => ['ssa', 'ass'], + 'text/x-subviewer' => ['sub'], + 'text/x-suse-ymp' => ['ymp'], + 'text/x-svhdr' => ['svh'], + 'text/x-svsrc' => ['sv'], + 'text/x-systemd-unit' => ['automount', 'device', 'mount', 'path', 'scope', 'service', 'slice', 'socket', 'swap', 'target', 'timer'], + 'text/x-tcl' => ['tcl', 'tk'], + 'text/x-tex' => ['tex', 'ltx', 'sty', 'cls', 'dtx', 'ins', 'latex'], + 'text/x-texinfo' => ['texi', 'texinfo'], + 'text/x-troff' => ['tr', 'roff', 't'], + 'text/x-troff-me' => ['me'], + 'text/x-troff-mm' => ['mm'], + 'text/x-troff-ms' => ['ms'], + 'text/x-twig' => ['twig'], + 'text/x-txt2tags' => ['t2t'], + 'text/x-uil' => ['uil'], + 'text/x-uuencode' => ['uu', 'uue'], + 'text/x-vala' => ['vala', 'vapi'], + 'text/x-vcalendar' => ['vcs', 'ics'], + 'text/x-vcard' => ['vcf', 'vcard', 'vct', 'gcrd'], + 'text/x-verilog' => ['v'], + 'text/x-vhdl' => ['vhd', 'vhdl'], + 'text/x-xmi' => ['xmi'], + 'text/x-xslfo' => ['fo', 'xslfo'], + 'text/x-yaml' => ['yaml', 'yml'], + 'text/x.gcode' => ['gcode'], + 'text/xml' => ['xml', 'xbl', 'xsd', 'rng'], + 'text/xml-external-parsed-entity' => ['ent'], + 'text/yaml' => ['yaml', 'yml'], + 'video/3gp' => ['3gp', '3gpp', '3ga'], + 'video/3gpp' => ['3gp', '3gpp', '3ga'], + 'video/3gpp-encrypted' => ['3gp', '3gpp', '3ga'], + 'video/3gpp2' => ['3g2', '3gp2', '3gpp2'], + 'video/annodex' => ['axv'], + 'video/avi' => ['avi', 'avf', 'divx'], + 'video/divx' => ['avi', 'avf', 'divx'], + 'video/dv' => ['dv'], + 'video/fli' => ['fli', 'flc'], + 'video/flv' => ['flv'], + 'video/h261' => ['h261'], + 'video/h263' => ['h263'], + 'video/h264' => ['h264'], + 'video/iso.segment' => ['m4s'], + 'video/jpeg' => ['jpgv'], + 'video/jpm' => ['jpm', 'jpgm'], + 'video/mj2' => ['mj2', 'mjp2'], + 'video/mp2t' => ['ts', 'm2t', 'm2ts', 'mts', 'cpi', 'clpi', 'mpl', 'mpls', 'bdm', 'bdmv'], + 'video/mp4' => ['mp4', 'mp4v', 'mpg4', 'm4v', 'f4v', 'lrv'], + 'video/mp4v-es' => ['mp4', 'm4v', 'f4v', 'lrv'], + 'video/mpeg' => ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v', 'mp2', 'vob'], + 'video/mpeg-system' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/msvideo' => ['avi', 'avf', 'divx'], + 'video/ogg' => ['ogv', 'ogg'], + 'video/quicktime' => ['mov', 'qt', 'moov', 'qtvr'], + 'video/vivo' => ['viv', 'vivo'], + 'video/vnd.dece.hd' => ['uvh', 'uvvh'], + 'video/vnd.dece.mobile' => ['uvm', 'uvvm'], + 'video/vnd.dece.pd' => ['uvp', 'uvvp'], + 'video/vnd.dece.sd' => ['uvs', 'uvvs'], + 'video/vnd.dece.video' => ['uvv', 'uvvv'], + 'video/vnd.divx' => ['avi', 'avf', 'divx'], + 'video/vnd.dvb.file' => ['dvb'], + 'video/vnd.fvt' => ['fvt'], + 'video/vnd.mpegurl' => ['mxu', 'm4u', 'm1u'], + 'video/vnd.ms-playready.media.pyv' => ['pyv'], + 'video/vnd.radgamettools.bink' => ['bik', 'bk2'], + 'video/vnd.radgamettools.smacker' => ['smk'], + 'video/vnd.rn-realvideo' => ['rv', 'rvx'], + 'video/vnd.uvvu.mp4' => ['uvu', 'uvvu'], + 'video/vnd.vivo' => ['viv', 'vivo'], + 'video/webm' => ['webm'], + 'video/x-anim' => ['anim[1-9j]'], + 'video/x-annodex' => ['axv'], + 'video/x-avi' => ['avi', 'avf', 'divx'], + 'video/x-f4v' => ['f4v'], + 'video/x-fli' => ['fli', 'flc'], + 'video/x-flic' => ['fli', 'flc'], + 'video/x-flv' => ['flv'], + 'video/x-javafx' => ['fxm'], + 'video/x-m4v' => ['m4v', 'mp4', 'f4v', 'lrv'], + 'video/x-matroska' => ['mkv', 'mk3d', 'mks'], + 'video/x-matroska-3d' => ['mk3d'], + 'video/x-mjpeg' => ['mjpeg', 'mjpg'], + 'video/x-mng' => ['mng'], + 'video/x-mpeg' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/x-mpeg-system' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/x-mpeg2' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/x-mpegurl' => ['m1u', 'm4u', 'mxu'], + 'video/x-ms-asf' => ['asf', 'asx'], + 'video/x-ms-asf-plugin' => ['asf'], + 'video/x-ms-vob' => ['vob'], + 'video/x-ms-wax' => ['asx', 'wax', 'wvx', 'wmx'], + 'video/x-ms-wm' => ['wm', 'asf'], + 'video/x-ms-wmv' => ['wmv'], + 'video/x-ms-wmx' => ['wmx', 'asx', 'wax', 'wvx'], + 'video/x-ms-wvx' => ['wvx', 'asx', 'wax', 'wmx'], + 'video/x-msvideo' => ['avi', 'avf', 'divx'], + 'video/x-nsv' => ['nsv'], + 'video/x-ogg' => ['ogv', 'ogg'], + 'video/x-ogm' => ['ogm'], + 'video/x-ogm+ogg' => ['ogm'], + 'video/x-real-video' => ['rv', 'rvx'], + 'video/x-sgi-movie' => ['movie'], + 'video/x-smv' => ['smv'], + 'video/x-theora' => ['ogg'], + 'video/x-theora+ogg' => ['ogg'], + 'x-conference/x-cooltalk' => ['ice'], + 'x-epoc/x-sisx-app' => ['sisx'], + 'zz-application/zz-winassoc-123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'zz-application/zz-winassoc-cab' => ['cab'], + 'zz-application/zz-winassoc-cdr' => ['cdr'], + 'zz-application/zz-winassoc-doc' => ['doc'], + 'zz-application/zz-winassoc-hlp' => ['hlp'], + 'zz-application/zz-winassoc-mdb' => ['mdb'], + 'zz-application/zz-winassoc-uu' => ['uue'], + 'zz-application/zz-winassoc-xls' => ['xls', 'xlc', 'xll', 'xlm', 'xlw', 'xla', 'xlt', 'xld'], + ]; + + private const REVERSE_MAP = [ + '1km' => ['application/vnd.1000minds.decision-model+xml'], + '32x' => ['application/x-genesis-32x-rom'], + '3dml' => ['text/vnd.in3d.3dml'], + '3ds' => ['application/x-nintendo-3ds-rom', 'image/x-3ds'], + '3dsx' => ['application/x-nintendo-3ds-executable'], + '3g2' => ['audio/3gpp2', 'video/3gpp2'], + '3ga' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], + '3gp' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], + '3gp2' => ['audio/3gpp2', 'video/3gpp2'], + '3gpp' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], + '3gpp2' => ['audio/3gpp2', 'video/3gpp2'], + '3mf' => ['model/3mf'], + '7z' => ['application/x-7z-compressed'], + '7z.001' => ['application/x-7z-compressed'], + 'BLEND' => ['application/x-blender'], + 'C' => ['text/x-c++src'], + 'PAR2' => ['application/x-par2'], + 'PL' => ['application/x-perl', 'text/x-perl'], + 'Z' => ['application/x-compress'], + 'a' => ['application/x-archive'], + 'a26' => ['application/x-atari-2600-rom'], + 'a78' => ['application/x-atari-7800-rom'], + 'aa' => ['audio/vnd.audible', 'audio/x-pn-audibleaudio'], + 'aab' => ['application/x-authorware-bin'], + 'aac' => ['audio/aac', 'audio/x-aac', 'audio/x-hx-aac-adts'], + 'aam' => ['application/x-authorware-map'], + 'aas' => ['application/x-authorware-seg'], + 'aax' => ['audio/vnd.audible', 'audio/vnd.audible.aax', 'audio/x-pn-audibleaudio'], + 'abw' => ['application/x-abiword'], + 'abw.CRASHED' => ['application/x-abiword'], + 'abw.gz' => ['application/x-abiword'], + 'ac' => ['application/pkix-attr-cert', 'application/vnd.nokia.n-gage.ac+xml'], + 'ac3' => ['audio/ac3'], + 'acc' => ['application/vnd.americandynamics.acc'], + 'ace' => ['application/x-ace', 'application/x-ace-compressed'], + 'acu' => ['application/vnd.acucobol'], + 'acutc' => ['application/vnd.acucorp'], + 'adb' => ['text/x-adasrc'], + 'adf' => ['application/x-amiga-disk-format'], + 'adp' => ['audio/adpcm'], + 'ads' => ['text/x-adasrc'], + 'adts' => ['audio/aac', 'audio/x-aac', 'audio/x-hx-aac-adts'], + 'aep' => ['application/vnd.audiograph'], + 'afm' => ['application/x-font-afm', 'application/x-font-type1'], + 'afp' => ['application/vnd.ibm.modcap'], + 'ag' => ['image/x-applix-graphics'], + 'agb' => ['application/x-gba-rom'], + 'ahead' => ['application/vnd.ahead.space'], + 'ai' => ['application/illustrator', 'application/postscript', 'application/vnd.adobe.illustrator'], + 'aif' => ['audio/x-aiff'], + 'aifc' => ['audio/x-aifc', 'audio/x-aiff', 'audio/x-aiffc'], + 'aiff' => ['audio/x-aiff'], + 'aiffc' => ['audio/x-aifc', 'audio/x-aiffc'], + 'air' => ['application/vnd.adobe.air-application-installer-package+zip'], + 'ait' => ['application/vnd.dvb.ait'], + 'al' => ['application/x-perl', 'text/x-perl'], + 'alz' => ['application/x-alz'], + 'ami' => ['application/vnd.amiga.ami'], + 'amr' => ['audio/amr', 'audio/amr-encrypted'], + 'amz' => ['audio/x-amzxml'], + 'ani' => ['application/x-navi-animation'], + 'anim[1-9j]' => ['video/x-anim'], + 'anx' => ['application/annodex', 'application/x-annodex'], + 'ape' => ['audio/x-ape'], + 'apk' => ['application/vnd.android.package-archive'], + 'apng' => ['image/apng'], + 'appcache' => ['text/cache-manifest'], + 'appimage' => ['application/vnd.appimage', 'application/x-iso9660-appimage'], + 'application' => ['application/x-ms-application'], + 'apr' => ['application/vnd.lotus-approach'], + 'ar' => ['application/x-archive'], + 'arc' => ['application/x-freearc'], + 'arj' => ['application/x-arj'], + 'arw' => ['image/x-sony-arw'], + 'as' => ['application/x-applix-spreadsheet'], + 'asc' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature', 'text/plain'], + 'asd' => ['text/x-common-lisp'], + 'asf' => ['application/vnd.ms-asf', 'video/x-ms-asf', 'video/x-ms-asf-plugin', 'video/x-ms-wm'], + 'asice' => ['application/vnd.etsi.asic-e+zip'], + 'asm' => ['text/x-asm'], + 'aso' => ['application/vnd.accpac.simply.aso'], + 'asp' => ['application/x-asp'], + 'ass' => ['audio/aac', 'audio/x-aac', 'audio/x-hx-aac-adts', 'text/x-ssa'], + 'astc' => ['image/astc'], + 'asx' => ['application/x-ms-asx', 'audio/x-ms-asx', 'video/x-ms-asf', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'atc' => ['application/vnd.acucorp'], + 'atom' => ['application/atom+xml'], + 'atomcat' => ['application/atomcat+xml'], + 'atomdeleted' => ['application/atomdeleted+xml'], + 'atomsvc' => ['application/atomsvc+xml'], + 'atx' => ['application/vnd.antix.game-component'], + 'au' => ['audio/basic'], + 'automount' => ['text/x-systemd-unit'], + 'avf' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'avi' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'avif' => ['image/avif', 'image/avif-sequence'], + 'avifs' => ['image/avif', 'image/avif-sequence'], + 'aw' => ['application/applixware', 'application/x-applix-word'], + 'awb' => ['audio/amr-wb', 'audio/amr-wb-encrypted'], + 'awk' => ['application/x-awk'], + 'axa' => ['audio/annodex', 'audio/x-annodex'], + 'axv' => ['video/annodex', 'video/x-annodex'], + 'azf' => ['application/vnd.airzip.filesecure.azf'], + 'azs' => ['application/vnd.airzip.filesecure.azs'], + 'azv' => ['image/vnd.airzip.accelerator.azv'], + 'azw' => ['application/vnd.amazon.ebook'], + 'azw3' => ['application/vnd.amazon.mobi8-ebook', 'application/x-mobi8-ebook'], + 'b16' => ['image/vnd.pco.b16'], + 'bak' => ['application/x-trash'], + 'bat' => ['application/x-msdownload'], + 'bcpio' => ['application/x-bcpio'], + 'bdf' => ['application/x-font-bdf'], + 'bdm' => ['application/vnd.syncml.dm+wbxml', 'video/mp2t'], + 'bdmv' => ['video/mp2t'], + 'bdoc' => ['application/bdoc', 'application/x-bdoc'], + 'bed' => ['application/vnd.realvnc.bed'], + 'bh2' => ['application/vnd.fujitsu.oasysprs'], + 'bib' => ['text/x-bibtex'], + 'bik' => ['video/vnd.radgamettools.bink'], + 'bin' => ['application/octet-stream'], + 'bk2' => ['video/vnd.radgamettools.bink'], + 'blb' => ['application/x-blorb'], + 'blend' => ['application/x-blender'], + 'blender' => ['application/x-blender'], + 'blorb' => ['application/x-blorb'], + 'bmi' => ['application/vnd.bmi'], + 'bmml' => ['application/vnd.balsamiq.bmml+xml'], + 'bmp' => ['image/bmp', 'image/x-bmp', 'image/x-ms-bmp'], + 'book' => ['application/vnd.framemaker'], + 'box' => ['application/vnd.previewsystems.box'], + 'boz' => ['application/x-bzip2'], + 'bps' => ['application/x-bps-patch'], + 'bsdiff' => ['application/x-bsdiff'], + 'bsp' => ['model/vnd.valve.source.compiled-map'], + 'btif' => ['image/prs.btif'], + 'bz' => ['application/bzip2', 'application/x-bzip', 'application/x-bzip2'], + 'bz2' => ['application/x-bz2', 'application/bzip2', 'application/x-bzip', 'application/x-bzip2'], + 'c' => ['text/x-c', 'text/x-csrc'], + 'c++' => ['text/x-c++src'], + 'c11amc' => ['application/vnd.cluetrust.cartomobile-config'], + 'c11amz' => ['application/vnd.cluetrust.cartomobile-config-pkg'], + 'c4d' => ['application/vnd.clonk.c4group'], + 'c4f' => ['application/vnd.clonk.c4group'], + 'c4g' => ['application/vnd.clonk.c4group'], + 'c4p' => ['application/vnd.clonk.c4group'], + 'c4u' => ['application/vnd.clonk.c4group'], + 'cab' => ['application/vnd.ms-cab-compressed', 'zz-application/zz-winassoc-cab'], + 'caf' => ['audio/x-caf'], + 'cap' => ['application/pcap', 'application/vnd.tcpdump.pcap', 'application/x-pcap'], + 'car' => ['application/vnd.curl.car'], + 'cat' => ['application/vnd.ms-pki.seccat'], + 'cb7' => ['application/x-cb7', 'application/x-cbr'], + 'cba' => ['application/x-cbr'], + 'cbl' => ['text/x-cobol'], + 'cbr' => ['application/vnd.comicbook-rar', 'application/x-cbr'], + 'cbt' => ['application/x-cbr', 'application/x-cbt'], + 'cbz' => ['application/vnd.comicbook+zip', 'application/x-cbr', 'application/x-cbz'], + 'cc' => ['text/x-c', 'text/x-c++src'], + 'cci' => ['application/x-nintendo-3ds-rom'], + 'ccmx' => ['application/x-ccmx'], + 'cco' => ['application/x-cocoa'], + 'cct' => ['application/x-director'], + 'ccxml' => ['application/ccxml+xml'], + 'cdbcmsg' => ['application/vnd.contact.cmsg'], + 'cdf' => ['application/x-netcdf'], + 'cdfx' => ['application/cdfx+xml'], + 'cdi' => ['application/x-discjuggler-cd-image'], + 'cdkey' => ['application/vnd.mediastation.cdkey'], + 'cdmia' => ['application/cdmi-capability'], + 'cdmic' => ['application/cdmi-container'], + 'cdmid' => ['application/cdmi-domain'], + 'cdmio' => ['application/cdmi-object'], + 'cdmiq' => ['application/cdmi-queue'], + 'cdr' => ['application/cdr', 'application/coreldraw', 'application/vnd.corel-draw', 'application/x-cdr', 'application/x-coreldraw', 'image/cdr', 'image/x-cdr', 'zz-application/zz-winassoc-cdr'], + 'cdx' => ['chemical/x-cdx'], + 'cdxml' => ['application/vnd.chemdraw+xml'], + 'cdy' => ['application/vnd.cinderella'], + 'cer' => ['application/pkix-cert'], + 'cert' => ['application/x-x509-ca-cert'], + 'cfs' => ['application/x-cfs-compressed'], + 'cgb' => ['application/x-gameboy-color-rom'], + 'cgm' => ['image/cgm'], + 'chat' => ['application/x-chat'], + 'chd' => ['application/x-mame-chd'], + 'chm' => ['application/vnd.ms-htmlhelp', 'application/x-chm'], + 'chrt' => ['application/vnd.kde.kchart', 'application/x-kchart'], + 'cif' => ['chemical/x-cif'], + 'cii' => ['application/vnd.anser-web-certificate-issue-initiation'], + 'cil' => ['application/vnd.ms-artgalry'], + 'cjs' => ['application/node'], + 'cl' => ['text/x-opencl-src'], + 'cla' => ['application/vnd.claymore'], + 'class' => ['application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java', 'application/x-java-class', 'application/x-java-vm'], + 'clkk' => ['application/vnd.crick.clicker.keyboard'], + 'clkp' => ['application/vnd.crick.clicker.palette'], + 'clkt' => ['application/vnd.crick.clicker.template'], + 'clkw' => ['application/vnd.crick.clicker.wordbank'], + 'clkx' => ['application/vnd.crick.clicker'], + 'clp' => ['application/x-msclip'], + 'clpi' => ['video/mp2t'], + 'cls' => ['application/x-tex', 'text/x-tex'], + 'cmake' => ['text/x-cmake'], + 'cmc' => ['application/vnd.cosmocaller'], + 'cmdf' => ['chemical/x-cmdf'], + 'cml' => ['chemical/x-cml'], + 'cmp' => ['application/vnd.yellowriver-custom-menu'], + 'cmx' => ['image/x-cmx'], + 'cob' => ['text/x-cobol'], + 'cod' => ['application/vnd.rim.cod'], + 'coffee' => ['application/vnd.coffeescript', 'text/coffeescript'], + 'com' => ['application/x-msdownload'], + 'conf' => ['text/plain'], + 'cpi' => ['video/mp2t'], + 'cpio' => ['application/x-cpio'], + 'cpio.gz' => ['application/x-cpio-compressed'], + 'cpp' => ['text/x-c', 'text/x-c++src'], + 'cpt' => ['application/mac-compactpro'], + 'cr' => ['text/crystal', 'text/x-crystal'], + 'cr2' => ['image/x-canon-cr2'], + 'cr3' => ['image/x-canon-cr3'], + 'crd' => ['application/x-mscardfile'], + 'crdownload' => ['application/x-partial-download'], + 'crl' => ['application/pkix-crl'], + 'crt' => ['application/x-x509-ca-cert'], + 'crw' => ['image/x-canon-crw'], + 'crx' => ['application/x-chrome-extension'], + 'cryptonote' => ['application/vnd.rig.cryptonote'], + 'cs' => ['text/x-csharp'], + 'csh' => ['application/x-csh'], + 'csl' => ['application/vnd.citationstyles.style+xml'], + 'csml' => ['chemical/x-csml'], + 'cso' => ['application/x-compressed-iso'], + 'csp' => ['application/vnd.commonspace'], + 'css' => ['text/css'], + 'cst' => ['application/x-director'], + 'csv' => ['text/csv', 'application/csv', 'text/x-comma-separated-values', 'text/x-csv'], + 'csvs' => ['text/csv-schema'], + 'cu' => ['application/cu-seeme'], + 'cue' => ['application/x-cue'], + 'cur' => ['image/x-win-bitmap'], + 'curl' => ['text/vnd.curl'], + 'cwk' => ['application/x-appleworks-document'], + 'cww' => ['application/prs.cww'], + 'cxt' => ['application/x-director'], + 'cxx' => ['text/x-c', 'text/x-c++src'], + 'd' => ['text/x-dsrc'], + 'dae' => ['model/vnd.collada+xml'], + 'daf' => ['application/vnd.mobius.daf'], + 'dar' => ['application/x-dar'], + 'dart' => ['application/vnd.dart', 'text/x-dart'], + 'dataless' => ['application/vnd.fdsn.seed'], + 'davmount' => ['application/davmount+xml'], + 'dbf' => ['application/dbase', 'application/dbf', 'application/vnd.dbf', 'application/x-dbase', 'application/x-dbf'], + 'dbk' => ['application/docbook+xml', 'application/vnd.oasis.docbook+xml', 'application/x-docbook+xml'], + 'dc' => ['application/x-dc-rom'], + 'dcl' => ['text/x-dcl'], + 'dcm' => ['application/dicom'], + 'dcr' => ['application/x-director', 'image/x-kodak-dcr'], + 'dcurl' => ['text/vnd.curl.dcurl'], + 'dd2' => ['application/vnd.oma.dd2+xml'], + 'ddd' => ['application/vnd.fujixerox.ddd'], + 'ddf' => ['application/vnd.syncml.dmddf+xml'], + 'dds' => ['image/vnd.ms-dds', 'image/x-dds'], + 'deb' => ['application/vnd.debian.binary-package', 'application/x-deb', 'application/x-debian-package'], + 'def' => ['text/plain'], + 'der' => ['application/x-x509-ca-cert'], + 'desktop' => ['application/x-desktop', 'application/x-gnome-app-info'], + 'device' => ['text/x-systemd-unit'], + 'dfac' => ['application/vnd.dreamfactory'], + 'dgc' => ['application/x-dgc-compressed'], + 'di' => ['text/x-dsrc'], + 'dia' => ['application/x-dia-diagram'], + 'dib' => ['image/bmp', 'image/x-bmp', 'image/x-ms-bmp'], + 'dic' => ['text/x-c'], + 'diff' => ['text/x-diff', 'text/x-patch'], + 'dir' => ['application/x-director'], + 'dis' => ['application/vnd.mobius.dis'], + 'disposition-notification' => ['message/disposition-notification'], + 'divx' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'djv' => ['image/vnd.djvu', 'image/vnd.djvu+multipage', 'image/x-djvu', 'image/x.djvu'], + 'djvu' => ['image/vnd.djvu', 'image/vnd.djvu+multipage', 'image/x-djvu', 'image/x.djvu'], + 'dll' => ['application/x-msdownload'], + 'dmg' => ['application/x-apple-diskimage'], + 'dmp' => ['application/pcap', 'application/vnd.tcpdump.pcap', 'application/x-pcap'], + 'dna' => ['application/vnd.dna'], + 'dng' => ['image/x-adobe-dng'], + 'doc' => ['application/msword', 'application/vnd.ms-word', 'application/x-msword', 'zz-application/zz-winassoc-doc'], + 'docbook' => ['application/docbook+xml', 'application/vnd.oasis.docbook+xml', 'application/x-docbook+xml'], + 'docm' => ['application/vnd.ms-word.document.macroenabled.12'], + 'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + 'dot' => ['application/msword', 'application/msword-template', 'text/vnd.graphviz'], + 'dotm' => ['application/vnd.ms-word.template.macroenabled.12'], + 'dotx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.template'], + 'dp' => ['application/vnd.osgi.dp'], + 'dpg' => ['application/vnd.dpgraph'], + 'dra' => ['audio/vnd.dra'], + 'drle' => ['image/dicom-rle'], + 'dsc' => ['text/prs.lines.tag'], + 'dsl' => ['text/x-dsl'], + 'dssc' => ['application/dssc+der'], + 'dtb' => ['application/x-dtbook+xml'], + 'dtd' => ['application/xml-dtd', 'text/x-dtd'], + 'dts' => ['audio/vnd.dts', 'audio/x-dts'], + 'dtshd' => ['audio/vnd.dts.hd', 'audio/x-dtshd'], + 'dtx' => ['application/x-tex', 'text/x-tex'], + 'dv' => ['video/dv'], + 'dvb' => ['video/vnd.dvb.file'], + 'dvi' => ['application/x-dvi'], + 'dvi.bz2' => ['application/x-bzdvi'], + 'dvi.gz' => ['application/x-gzdvi'], + 'dwd' => ['application/atsc-dwd+xml'], + 'dwf' => ['model/vnd.dwf'], + 'dwg' => ['image/vnd.dwg'], + 'dxf' => ['image/vnd.dxf'], + 'dxp' => ['application/vnd.spotfire.dxp'], + 'dxr' => ['application/x-director'], + 'e' => ['text/x-eiffel'], + 'ear' => ['application/java-archive'], + 'ecelp4800' => ['audio/vnd.nuera.ecelp4800'], + 'ecelp7470' => ['audio/vnd.nuera.ecelp7470'], + 'ecelp9600' => ['audio/vnd.nuera.ecelp9600'], + 'ecma' => ['application/ecmascript'], + 'edm' => ['application/vnd.novadigm.edm'], + 'edx' => ['application/vnd.novadigm.edx'], + 'efif' => ['application/vnd.picsel'], + 'egon' => ['application/x-egon'], + 'ei6' => ['application/vnd.pg.osasli'], + 'eif' => ['text/x-eiffel'], + 'el' => ['text/x-emacs-lisp'], + 'emf' => ['application/emf', 'application/x-emf', 'application/x-msmetafile', 'image/emf', 'image/x-emf'], + 'eml' => ['message/rfc822'], + 'emma' => ['application/emma+xml'], + 'emotionml' => ['application/emotionml+xml'], + 'emp' => ['application/vnd.emusic-emusic_package'], + 'emz' => ['application/x-msmetafile'], + 'ent' => ['application/xml-external-parsed-entity', 'text/xml-external-parsed-entity'], + 'eol' => ['audio/vnd.digital-winds'], + 'eot' => ['application/vnd.ms-fontobject'], + 'eps' => ['application/postscript', 'image/x-eps'], + 'eps.bz2' => ['image/x-bzeps'], + 'eps.gz' => ['image/x-gzeps'], + 'epsf' => ['image/x-eps'], + 'epsf.bz2' => ['image/x-bzeps'], + 'epsf.gz' => ['image/x-gzeps'], + 'epsi' => ['image/x-eps'], + 'epsi.bz2' => ['image/x-bzeps'], + 'epsi.gz' => ['image/x-gzeps'], + 'epub' => ['application/epub+zip'], + 'erl' => ['text/x-erlang'], + 'es' => ['application/ecmascript', 'text/ecmascript'], + 'es3' => ['application/vnd.eszigno3+xml'], + 'esa' => ['application/vnd.osgi.subsystem'], + 'esf' => ['application/vnd.epson.esf'], + 'et3' => ['application/vnd.eszigno3+xml'], + 'etheme' => ['application/x-e-theme'], + 'etx' => ['text/x-setext'], + 'eva' => ['application/x-eva'], + 'evy' => ['application/x-envoy'], + 'ex' => ['text/x-elixir'], + 'exe' => ['application/x-ms-dos-executable', 'application/x-msdos-program', 'application/x-msdownload'], + 'exi' => ['application/exi'], + 'exr' => ['image/aces', 'image/x-exr'], + 'exs' => ['text/x-elixir'], + 'ext' => ['application/vnd.novadigm.ext'], + 'ez' => ['application/andrew-inset'], + 'ez2' => ['application/vnd.ezpix-album'], + 'ez3' => ['application/vnd.ezpix-package'], + 'f' => ['text/x-fortran'], + 'f4a' => ['audio/m4a', 'audio/mp4', 'audio/x-m4a'], + 'f4b' => ['audio/x-m4b'], + 'f4v' => ['video/mp4', 'video/mp4v-es', 'video/x-f4v', 'video/x-m4v'], + 'f77' => ['text/x-fortran'], + 'f90' => ['text/x-fortran'], + 'f95' => ['text/x-fortran'], + 'fasl' => ['text/x-common-lisp'], + 'fb2' => ['application/x-fictionbook', 'application/x-fictionbook+xml'], + 'fb2.zip' => ['application/x-zip-compressed-fb2'], + 'fbs' => ['image/vnd.fastbidsheet'], + 'fcdt' => ['application/vnd.adobe.formscentral.fcdt'], + 'fcs' => ['application/vnd.isac.fcs'], + 'fd' => ['application/x-fd-file', 'application/x-raw-floppy-disk-image'], + 'fdf' => ['application/vnd.fdf'], + 'fds' => ['application/x-fds-disk'], + 'fdt' => ['application/fdt+xml'], + 'fe_launch' => ['application/vnd.denovo.fcselayout-link'], + 'feature' => ['text/x-gherkin'], + 'fg5' => ['application/vnd.fujitsu.oasysgp'], + 'fgd' => ['application/x-director'], + 'fh' => ['image/x-freehand'], + 'fh4' => ['image/x-freehand'], + 'fh5' => ['image/x-freehand'], + 'fh7' => ['image/x-freehand'], + 'fhc' => ['image/x-freehand'], + 'fig' => ['application/x-xfig', 'image/x-xfig'], + 'fits' => ['image/fits', 'image/x-fits'], + 'fl' => ['application/x-fluid'], + 'flac' => ['audio/flac', 'audio/x-flac'], + 'flatpak' => ['application/vnd.flatpak', 'application/vnd.xdgapp'], + 'flatpakref' => ['application/vnd.flatpak.ref'], + 'flatpakrepo' => ['application/vnd.flatpak.repo'], + 'flc' => ['video/fli', 'video/x-fli', 'video/x-flic'], + 'fli' => ['video/fli', 'video/x-fli', 'video/x-flic'], + 'flo' => ['application/vnd.micrografx.flo'], + 'flv' => ['video/x-flv', 'application/x-flash-video', 'flv-application/octet-stream', 'video/flv'], + 'flw' => ['application/vnd.kde.kivio', 'application/x-kivio'], + 'flx' => ['text/vnd.fmi.flexstor'], + 'fly' => ['text/vnd.fly'], + 'fm' => ['application/vnd.framemaker', 'application/x-frame'], + 'fnc' => ['application/vnd.frogans.fnc'], + 'fo' => ['application/vnd.software602.filler.form+xml', 'text/x-xslfo'], + 'fodg' => ['application/vnd.oasis.opendocument.graphics-flat-xml'], + 'fodp' => ['application/vnd.oasis.opendocument.presentation-flat-xml'], + 'fods' => ['application/vnd.oasis.opendocument.spreadsheet-flat-xml'], + 'fodt' => ['application/vnd.oasis.opendocument.text-flat-xml'], + 'for' => ['text/x-fortran'], + 'fpx' => ['image/vnd.fpx'], + 'frame' => ['application/vnd.framemaker'], + 'fsc' => ['application/vnd.fsc.weblaunch'], + 'fst' => ['image/vnd.fst'], + 'ftc' => ['application/vnd.fluxtime.clip'], + 'fti' => ['application/vnd.anser-web-funds-transfer-initiation'], + 'fvt' => ['video/vnd.fvt'], + 'fxm' => ['video/x-javafx'], + 'fxp' => ['application/vnd.adobe.fxp'], + 'fxpl' => ['application/vnd.adobe.fxp'], + 'fzs' => ['application/vnd.fuzzysheet'], + 'g2w' => ['application/vnd.geoplan'], + 'g3' => ['image/fax-g3', 'image/g3fax'], + 'g3w' => ['application/vnd.geospace'], + 'gac' => ['application/vnd.groove-account'], + 'gam' => ['application/x-tads'], + 'gb' => ['application/x-gameboy-rom'], + 'gba' => ['application/x-gba-rom'], + 'gbc' => ['application/x-gameboy-color-rom'], + 'gbr' => ['application/rpki-ghostbusters', 'image/x-gimp-gbr'], + 'gca' => ['application/x-gca-compressed'], + 'gcode' => ['text/x.gcode'], + 'gcrd' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'gdi' => ['application/x-gd-rom-cue'], + 'gdl' => ['model/vnd.gdl'], + 'gdoc' => ['application/vnd.google-apps.document'], + 'ged' => ['application/x-gedcom', 'text/gedcom'], + 'gedcom' => ['application/x-gedcom', 'text/gedcom'], + 'gem' => ['application/x-gtar', 'application/x-tar'], + 'gen' => ['application/x-genesis-rom'], + 'geo' => ['application/vnd.dynageo'], + 'geo.json' => ['application/geo+json', 'application/vnd.geo+json'], + 'geojson' => ['application/geo+json', 'application/vnd.geo+json'], + 'gex' => ['application/vnd.geometry-explorer'], + 'gf' => ['application/x-tex-gf'], + 'gg' => ['application/x-gamegear-rom'], + 'ggb' => ['application/vnd.geogebra.file'], + 'ggt' => ['application/vnd.geogebra.tool'], + 'ghf' => ['application/vnd.groove-help'], + 'gif' => ['image/gif'], + 'gih' => ['image/x-gimp-gih'], + 'gim' => ['application/vnd.groove-identity-message'], + 'glade' => ['application/x-glade'], + 'glb' => ['model/gltf-binary'], + 'gltf' => ['model/gltf+json'], + 'gml' => ['application/gml+xml'], + 'gmo' => ['application/x-gettext-translation'], + 'gmx' => ['application/vnd.gmx'], + 'gnc' => ['application/x-gnucash'], + 'gnd' => ['application/gnunet-directory'], + 'gnucash' => ['application/x-gnucash'], + 'gnumeric' => ['application/x-gnumeric'], + 'gnuplot' => ['application/x-gnuplot'], + 'go' => ['text/x-go'], + 'gp' => ['application/x-gnuplot'], + 'gpg' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature'], + 'gph' => ['application/vnd.flographit'], + 'gplt' => ['application/x-gnuplot'], + 'gpx' => ['application/gpx', 'application/gpx+xml', 'application/x-gpx', 'application/x-gpx+xml'], + 'gqf' => ['application/vnd.grafeq'], + 'gqs' => ['application/vnd.grafeq'], + 'gra' => ['application/x-graphite'], + 'gradle' => ['text/x-gradle'], + 'gram' => ['application/srgs'], + 'gramps' => ['application/x-gramps-xml'], + 'gre' => ['application/vnd.geometry-explorer'], + 'groovy' => ['text/x-groovy'], + 'grv' => ['application/vnd.groove-injector'], + 'grxml' => ['application/srgs+xml'], + 'gs' => ['text/x-genie'], + 'gsf' => ['application/x-font-ghostscript', 'application/x-font-type1'], + 'gsh' => ['text/x-groovy'], + 'gsheet' => ['application/vnd.google-apps.spreadsheet'], + 'gslides' => ['application/vnd.google-apps.presentation'], + 'gsm' => ['audio/x-gsm'], + 'gtar' => ['application/x-gtar', 'application/x-tar'], + 'gtm' => ['application/vnd.groove-tool-message'], + 'gtw' => ['model/vnd.gtw'], + 'gv' => ['text/vnd.graphviz'], + 'gvp' => ['text/google-video-pointer', 'text/x-google-video-pointer'], + 'gvy' => ['text/x-groovy'], + 'gxf' => ['application/gxf'], + 'gxt' => ['application/vnd.geonext'], + 'gy' => ['text/x-groovy'], + 'gz' => ['application/x-gzip', 'application/gzip'], + 'h' => ['text/x-c', 'text/x-chdr'], + 'h++' => ['text/x-c++hdr'], + 'h261' => ['video/h261'], + 'h263' => ['video/h263'], + 'h264' => ['video/h264'], + 'h4' => ['application/x-hdf'], + 'h5' => ['application/x-hdf'], + 'hal' => ['application/vnd.hal+xml'], + 'hbci' => ['application/vnd.hbci'], + 'hbs' => ['text/x-handlebars-template'], + 'hdd' => ['application/x-virtualbox-hdd'], + 'hdf' => ['application/x-hdf'], + 'hdf4' => ['application/x-hdf'], + 'hdf5' => ['application/x-hdf'], + 'heic' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], + 'heics' => ['image/heic-sequence'], + 'heif' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], + 'heifs' => ['image/heif-sequence'], + 'hej2' => ['image/hej2k'], + 'held' => ['application/atsc-held+xml'], + 'hfe' => ['application/x-hfe-file', 'application/x-hfe-floppy-image'], + 'hh' => ['text/x-c', 'text/x-c++hdr'], + 'hjson' => ['application/hjson'], + 'hlp' => ['application/winhlp', 'zz-application/zz-winassoc-hlp'], + 'hp' => ['text/x-c++hdr'], + 'hpgl' => ['application/vnd.hp-hpgl'], + 'hpid' => ['application/vnd.hp-hpid'], + 'hpp' => ['text/x-c++hdr'], + 'hps' => ['application/vnd.hp-hps'], + 'hqx' => ['application/stuffit', 'application/mac-binhex40'], + 'hs' => ['text/x-haskell'], + 'hsj2' => ['image/hsj2'], + 'htc' => ['text/x-component'], + 'htke' => ['application/vnd.kenameaapp'], + 'htm' => ['text/html', 'application/xhtml+xml'], + 'html' => ['text/html', 'application/xhtml+xml'], + 'hvd' => ['application/vnd.yamaha.hv-dic'], + 'hvp' => ['application/vnd.yamaha.hv-voice'], + 'hvs' => ['application/vnd.yamaha.hv-script'], + 'hwp' => ['application/vnd.haansoft-hwp', 'application/x-hwp'], + 'hwt' => ['application/vnd.haansoft-hwt', 'application/x-hwt'], + 'hxx' => ['text/x-c++hdr'], + 'i2g' => ['application/vnd.intergeo'], + 'ica' => ['application/x-ica'], + 'icb' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'icc' => ['application/vnd.iccprofile'], + 'ice' => ['x-conference/x-cooltalk'], + 'icm' => ['application/vnd.iccprofile'], + 'icns' => ['image/x-icns'], + 'ico' => ['application/ico', 'image/ico', 'image/icon', 'image/vnd.microsoft.icon', 'image/x-ico', 'image/x-icon', 'text/ico'], + 'ics' => ['application/ics', 'text/calendar', 'text/x-vcalendar'], + 'idl' => ['text/x-idl'], + 'ief' => ['image/ief'], + 'ifb' => ['text/calendar'], + 'iff' => ['image/x-iff', 'image/x-ilbm'], + 'ifm' => ['application/vnd.shana.informed.formdata'], + 'iges' => ['model/iges'], + 'igl' => ['application/vnd.igloader'], + 'igm' => ['application/vnd.insors.igm'], + 'igs' => ['model/iges'], + 'igx' => ['application/vnd.micrografx.igx'], + 'iif' => ['application/vnd.shana.informed.interchange'], + 'ilbm' => ['image/x-iff', 'image/x-ilbm'], + 'ime' => ['audio/imelody', 'audio/x-imelody', 'text/x-imelody'], + 'img' => ['application/x-raw-disk-image'], + 'img.xz' => ['application/x-raw-disk-image-xz-compressed'], + 'imp' => ['application/vnd.accpac.simply.imp'], + 'ims' => ['application/vnd.ms-ims'], + 'imy' => ['audio/imelody', 'audio/x-imelody', 'text/x-imelody'], + 'in' => ['text/plain'], + 'ini' => ['text/plain'], + 'ink' => ['application/inkml+xml'], + 'inkml' => ['application/inkml+xml'], + 'ins' => ['application/x-tex', 'text/x-tex'], + 'install' => ['application/x-install-instructions'], + 'iota' => ['application/vnd.astraea-software.iota'], + 'ipfix' => ['application/ipfix'], + 'ipk' => ['application/vnd.shana.informed.package'], + 'ips' => ['application/x-ips-patch'], + 'iptables' => ['text/x-iptables'], + 'ipynb' => ['application/x-ipynb+json'], + 'irm' => ['application/vnd.ibm.rights-management'], + 'irp' => ['application/vnd.irepository.package+xml'], + 'iso' => ['application/x-cd-image', 'application/x-dreamcast-rom', 'application/x-gamecube-iso-image', 'application/x-gamecube-rom', 'application/x-iso9660-image', 'application/x-saturn-rom', 'application/x-sega-cd-rom', 'application/x-sega-pico-rom', 'application/x-wbfs', 'application/x-wia', 'application/x-wii-iso-image', 'application/x-wii-rom'], + 'iso9660' => ['application/x-cd-image', 'application/x-iso9660-image'], + 'it' => ['audio/x-it'], + 'it87' => ['application/x-it87'], + 'itp' => ['application/vnd.shana.informed.formtemplate'], + 'its' => ['application/its+xml'], + 'ivp' => ['application/vnd.immervision-ivp'], + 'ivu' => ['application/vnd.immervision-ivu'], + 'j2c' => ['image/x-jp2-codestream'], + 'j2k' => ['image/x-jp2-codestream'], + 'jad' => ['text/vnd.sun.j2me.app-descriptor'], + 'jade' => ['text/jade'], + 'jam' => ['application/vnd.jam'], + 'jar' => ['application/x-java-archive', 'application/java-archive', 'application/x-jar'], + 'jardiff' => ['application/x-java-archive-diff'], + 'java' => ['text/x-java', 'text/x-java-source'], + 'jceks' => ['application/x-java-jce-keystore'], + 'jhc' => ['image/jphc'], + 'jisp' => ['application/vnd.jisp'], + 'jks' => ['application/x-java-keystore'], + 'jls' => ['image/jls'], + 'jlt' => ['application/vnd.hp-jlyt'], + 'jng' => ['image/x-jng'], + 'jnlp' => ['application/x-java-jnlp-file'], + 'joda' => ['application/vnd.joost.joda-archive'], + 'jp2' => ['image/jp2', 'image/jpeg2000', 'image/jpeg2000-image', 'image/x-jpeg2000-image'], + 'jpc' => ['image/x-jp2-codestream'], + 'jpe' => ['image/jpeg', 'image/pjpeg'], + 'jpeg' => ['image/jpeg', 'image/pjpeg'], + 'jpf' => ['image/jpx'], + 'jpg' => ['image/jpeg', 'image/pjpeg'], + 'jpg2' => ['image/jp2', 'image/jpeg2000', 'image/jpeg2000-image', 'image/x-jpeg2000-image'], + 'jpgm' => ['image/jpm', 'video/jpm'], + 'jpgv' => ['video/jpeg'], + 'jph' => ['image/jph'], + 'jpm' => ['image/jpm', 'video/jpm'], + 'jpr' => ['application/x-jbuilder-project'], + 'jpx' => ['application/x-jbuilder-project', 'image/jpx'], + 'jrd' => ['application/jrd+json'], + 'js' => ['text/javascript', 'application/javascript', 'application/x-javascript'], + 'jsm' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'json' => ['application/json', 'application/schema+json'], + 'json-patch' => ['application/json-patch+json'], + 'json5' => ['application/json5'], + 'jsonld' => ['application/ld+json'], + 'jsonml' => ['application/jsonml+json'], + 'jsx' => ['text/jsx'], + 'jxl' => ['image/jxl'], + 'jxr' => ['image/jxr'], + 'jxra' => ['image/jxra'], + 'jxrs' => ['image/jxrs'], + 'jxs' => ['image/jxs'], + 'jxsc' => ['image/jxsc'], + 'jxsi' => ['image/jxsi'], + 'jxss' => ['image/jxss'], + 'k25' => ['image/x-kodak-k25'], + 'k7' => ['application/x-thomson-cassette'], + 'kar' => ['audio/midi', 'audio/x-midi'], + 'karbon' => ['application/vnd.kde.karbon', 'application/x-karbon'], + 'kdbx' => ['application/x-keepass2'], + 'kdc' => ['image/x-kodak-kdc'], + 'kdelnk' => ['application/x-desktop', 'application/x-gnome-app-info'], + 'kexi' => ['application/x-kexiproject-sqlite', 'application/x-kexiproject-sqlite2', 'application/x-kexiproject-sqlite3', 'application/x-vnd.kde.kexi'], + 'kexic' => ['application/x-kexi-connectiondata'], + 'kexis' => ['application/x-kexiproject-shortcut'], + 'key' => ['application/vnd.apple.keynote', 'application/pgp-keys', 'application/x-iwork-keynote-sffkey'], + 'keynote' => ['application/vnd.apple.keynote'], + 'kfo' => ['application/vnd.kde.kformula', 'application/x-kformula'], + 'kfx' => ['application/vnd.amazon.mobi8-ebook', 'application/x-mobi8-ebook'], + 'kia' => ['application/vnd.kidspiration'], + 'kil' => ['application/x-killustrator'], + 'kino' => ['application/smil', 'application/smil+xml'], + 'kml' => ['application/vnd.google-earth.kml+xml'], + 'kmz' => ['application/vnd.google-earth.kmz'], + 'kne' => ['application/vnd.kinar'], + 'knp' => ['application/vnd.kinar'], + 'kon' => ['application/vnd.kde.kontour', 'application/x-kontour'], + 'kpm' => ['application/x-kpovmodeler'], + 'kpr' => ['application/vnd.kde.kpresenter', 'application/x-kpresenter'], + 'kpt' => ['application/vnd.kde.kpresenter', 'application/x-kpresenter'], + 'kpxx' => ['application/vnd.ds-keypoint'], + 'kra' => ['application/x-krita'], + 'krz' => ['application/x-krita'], + 'ks' => ['application/x-java-keystore'], + 'ksp' => ['application/vnd.kde.kspread', 'application/x-kspread'], + 'ksy' => ['text/x-kaitai-struct'], + 'kt' => ['text/x-kotlin'], + 'ktr' => ['application/vnd.kahootz'], + 'ktx' => ['image/ktx'], + 'ktx2' => ['image/ktx2'], + 'ktz' => ['application/vnd.kahootz'], + 'kud' => ['application/x-kugar'], + 'kwd' => ['application/vnd.kde.kword', 'application/x-kword'], + 'kwt' => ['application/vnd.kde.kword', 'application/x-kword'], + 'la' => ['application/x-shared-library-la'], + 'lasxml' => ['application/vnd.las.las+xml'], + 'latex' => ['application/x-latex', 'application/x-tex', 'text/x-tex'], + 'lbd' => ['application/vnd.llamagraphics.life-balance.desktop'], + 'lbe' => ['application/vnd.llamagraphics.life-balance.exchange+xml'], + 'lbm' => ['image/x-iff', 'image/x-ilbm'], + 'ldif' => ['text/x-ldif'], + 'les' => ['application/vnd.hhe.lesson-player'], + 'less' => ['text/less'], + 'lgr' => ['application/lgr+xml'], + 'lha' => ['application/x-lha', 'application/x-lzh-compressed'], + 'lhs' => ['text/x-literate-haskell'], + 'lhz' => ['application/x-lhz'], + 'link66' => ['application/vnd.route66.link66+xml'], + 'lisp' => ['text/x-common-lisp'], + 'list' => ['text/plain'], + 'list3820' => ['application/vnd.ibm.modcap'], + 'listafp' => ['application/vnd.ibm.modcap'], + 'litcoffee' => ['text/coffeescript'], + 'lnk' => ['application/x-ms-shortcut'], + 'lnx' => ['application/x-atari-lynx-rom'], + 'loas' => ['audio/usac'], + 'log' => ['text/plain', 'text/x-log'], + 'lostxml' => ['application/lost+xml'], + 'lrm' => ['application/vnd.ms-lrm'], + 'lrv' => ['video/mp4', 'video/mp4v-es', 'video/x-m4v'], + 'lrz' => ['application/x-lrzip'], + 'ltf' => ['application/vnd.frogans.ltf'], + 'ltx' => ['application/x-tex', 'text/x-tex'], + 'lua' => ['text/x-lua'], + 'luac' => ['application/x-lua-bytecode'], + 'lvp' => ['audio/vnd.lucent.voice'], + 'lwo' => ['image/x-lwo'], + 'lwob' => ['image/x-lwo'], + 'lwp' => ['application/vnd.lotus-wordpro'], + 'lws' => ['image/x-lws'], + 'ly' => ['text/x-lilypond'], + 'lyx' => ['application/x-lyx', 'text/x-lyx'], + 'lz' => ['application/x-lzip'], + 'lz4' => ['application/x-lz4'], + 'lzh' => ['application/x-lha', 'application/x-lzh-compressed'], + 'lzma' => ['application/x-lzma'], + 'lzo' => ['application/x-lzop'], + 'm' => ['text/x-matlab', 'text/x-objcsrc', 'text/x-octave'], + 'm13' => ['application/x-msmediaview'], + 'm14' => ['application/x-msmediaview'], + 'm15' => ['audio/x-mod'], + 'm1u' => ['video/vnd.mpegurl', 'video/x-mpegurl'], + 'm1v' => ['video/mpeg'], + 'm21' => ['application/mp21'], + 'm2a' => ['audio/mpeg'], + 'm2t' => ['video/mp2t'], + 'm2ts' => ['video/mp2t'], + 'm2v' => ['video/mpeg'], + 'm3a' => ['audio/mpeg'], + 'm3u' => ['audio/x-mpegurl', 'application/m3u', 'application/vnd.apple.mpegurl', 'audio/m3u', 'audio/mpegurl', 'audio/x-m3u', 'audio/x-mp3-playlist'], + 'm3u8' => ['application/m3u', 'application/vnd.apple.mpegurl', 'audio/m3u', 'audio/mpegurl', 'audio/x-m3u', 'audio/x-mp3-playlist', 'audio/x-mpegurl'], + 'm4' => ['application/x-m4'], + 'm4a' => ['audio/mp4', 'audio/m4a', 'audio/x-m4a'], + 'm4b' => ['audio/x-m4b'], + 'm4p' => ['application/mp4'], + 'm4r' => ['audio/x-m4r'], + 'm4s' => ['video/iso.segment'], + 'm4u' => ['video/vnd.mpegurl', 'video/x-mpegurl'], + 'm4v' => ['video/mp4', 'video/mp4v-es', 'video/x-m4v'], + 'm7' => ['application/x-thomson-cartridge-memo7'], + 'ma' => ['application/mathematica'], + 'mab' => ['application/x-markaby'], + 'mads' => ['application/mads+xml'], + 'maei' => ['application/mmt-aei+xml'], + 'mag' => ['application/vnd.ecowin.chart'], + 'mak' => ['text/x-makefile'], + 'maker' => ['application/vnd.framemaker'], + 'man' => ['application/x-troff-man', 'text/troff'], + 'manifest' => ['text/cache-manifest'], + 'map' => ['application/json'], + 'markdown' => ['text/markdown', 'text/x-markdown'], + 'mathml' => ['application/mathml+xml'], + 'mb' => ['application/mathematica'], + 'mbk' => ['application/vnd.mobius.mbk'], + 'mbox' => ['application/mbox'], + 'mc1' => ['application/vnd.medcalcdata'], + 'mc2' => ['text/vnd.senx.warpscript'], + 'mcd' => ['application/vnd.mcd'], + 'mcurl' => ['text/vnd.curl.mcurl'], + 'md' => ['text/markdown', 'text/x-markdown'], + 'mdb' => ['application/x-msaccess', 'application/mdb', 'application/msaccess', 'application/vnd.ms-access', 'application/vnd.msaccess', 'application/x-mdb', 'zz-application/zz-winassoc-mdb'], + 'mdi' => ['image/vnd.ms-modi'], + 'mdx' => ['application/x-genesis-32x-rom', 'text/mdx'], + 'me' => ['text/troff', 'text/x-troff-me'], + 'med' => ['audio/x-mod'], + 'mesh' => ['model/mesh'], + 'meta4' => ['application/metalink4+xml'], + 'metalink' => ['application/metalink+xml'], + 'mets' => ['application/mets+xml'], + 'mfm' => ['application/vnd.mfmp'], + 'mft' => ['application/rpki-manifest'], + 'mgp' => ['application/vnd.osgeo.mapguide.package', 'application/x-magicpoint'], + 'mgz' => ['application/vnd.proteus.magazine'], + 'mht' => ['application/x-mimearchive'], + 'mhtml' => ['application/x-mimearchive'], + 'mid' => ['audio/midi', 'audio/x-midi'], + 'midi' => ['audio/midi', 'audio/x-midi'], + 'mie' => ['application/x-mie'], + 'mif' => ['application/vnd.mif', 'application/x-mif'], + 'mime' => ['message/rfc822'], + 'minipsf' => ['audio/x-minipsf'], + 'mj2' => ['video/mj2'], + 'mjp2' => ['video/mj2'], + 'mjpeg' => ['video/x-mjpeg'], + 'mjpg' => ['video/x-mjpeg'], + 'mjs' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'mk' => ['text/x-makefile'], + 'mk3d' => ['video/x-matroska', 'video/x-matroska-3d'], + 'mka' => ['audio/x-matroska'], + 'mkd' => ['text/markdown', 'text/x-markdown'], + 'mks' => ['video/x-matroska'], + 'mkv' => ['video/x-matroska'], + 'ml' => ['text/x-ocaml'], + 'mli' => ['text/x-ocaml'], + 'mlp' => ['application/vnd.dolby.mlp'], + 'mm' => ['text/x-troff-mm'], + 'mmd' => ['application/vnd.chipnuts.karaoke-mmd'], + 'mmf' => ['application/vnd.smaf', 'application/x-smaf'], + 'mml' => ['application/mathml+xml', 'text/mathml'], + 'mmr' => ['image/vnd.fujixerox.edmics-mmr'], + 'mng' => ['video/x-mng'], + 'mny' => ['application/x-msmoney'], + 'mo' => ['application/x-gettext-translation', 'text/x-modelica'], + 'mo3' => ['audio/x-mo3'], + 'mobi' => ['application/x-mobipocket-ebook'], + 'moc' => ['text/x-moc'], + 'mod' => ['application/x-object', 'audio/x-mod'], + 'mods' => ['application/mods+xml'], + 'mof' => ['text/x-mof'], + 'moov' => ['video/quicktime'], + 'mount' => ['text/x-systemd-unit'], + 'mov' => ['video/quicktime'], + 'movie' => ['video/x-sgi-movie'], + 'mp+' => ['audio/x-musepack'], + 'mp2' => ['audio/mp2', 'audio/mpeg', 'audio/x-mp2', 'video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mp21' => ['application/mp21'], + 'mp2a' => ['audio/mpeg'], + 'mp3' => ['audio/mpeg', 'audio/mp3', 'audio/x-mp3', 'audio/x-mpeg', 'audio/x-mpg'], + 'mp4' => ['video/mp4', 'video/mp4v-es', 'video/x-m4v'], + 'mp4a' => ['audio/mp4'], + 'mp4s' => ['application/mp4'], + 'mp4v' => ['video/mp4'], + 'mpc' => ['application/vnd.mophun.certificate', 'audio/x-musepack'], + 'mpd' => ['application/dash+xml'], + 'mpe' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpeg' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpg' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpg4' => ['video/mp4'], + 'mpga' => ['audio/mp3', 'audio/mpeg', 'audio/x-mp3', 'audio/x-mpeg', 'audio/x-mpg'], + 'mpkg' => ['application/vnd.apple.installer+xml'], + 'mpl' => ['video/mp2t'], + 'mpls' => ['video/mp2t'], + 'mpm' => ['application/vnd.blueice.multipass'], + 'mpn' => ['application/vnd.mophun.application'], + 'mpp' => ['application/vnd.ms-project', 'audio/x-musepack'], + 'mpt' => ['application/vnd.ms-project'], + 'mpy' => ['application/vnd.ibm.minipay'], + 'mqy' => ['application/vnd.mobius.mqy'], + 'mrc' => ['application/marc'], + 'mrcx' => ['application/marcxml+xml'], + 'mrl' => ['text/x-mrml'], + 'mrml' => ['text/x-mrml'], + 'mrw' => ['image/x-minolta-mrw'], + 'ms' => ['text/troff', 'text/x-troff-ms'], + 'mscml' => ['application/mediaservercontrol+xml'], + 'mseed' => ['application/vnd.fdsn.mseed'], + 'mseq' => ['application/vnd.mseq'], + 'msf' => ['application/vnd.epson.msf'], + 'msg' => ['application/vnd.ms-outlook'], + 'msh' => ['model/mesh'], + 'msi' => ['application/x-msdownload', 'application/x-msi'], + 'msl' => ['application/vnd.mobius.msl'], + 'msod' => ['image/x-msod'], + 'msty' => ['application/vnd.muvee.style'], + 'msx' => ['application/x-msx-rom'], + 'mtl' => ['model/mtl'], + 'mtm' => ['audio/x-mod'], + 'mts' => ['model/vnd.mts', 'video/mp2t'], + 'mup' => ['text/x-mup'], + 'mus' => ['application/vnd.musician'], + 'musd' => ['application/mmt-usd+xml'], + 'musicxml' => ['application/vnd.recordare.musicxml+xml'], + 'mvb' => ['application/x-msmediaview'], + 'mvt' => ['application/vnd.mapbox-vector-tile'], + 'mwf' => ['application/vnd.mfer'], + 'mxf' => ['application/mxf'], + 'mxl' => ['application/vnd.recordare.musicxml'], + 'mxmf' => ['audio/mobile-xmf'], + 'mxml' => ['application/xv+xml'], + 'mxs' => ['application/vnd.triscape.mxs'], + 'mxu' => ['video/vnd.mpegurl', 'video/x-mpegurl'], + 'n-gage' => ['application/vnd.nokia.n-gage.symbian.install'], + 'n3' => ['text/n3'], + 'n64' => ['application/x-n64-rom'], + 'nb' => ['application/mathematica', 'application/x-mathematica'], + 'nbp' => ['application/vnd.wolfram.player'], + 'nc' => ['application/x-netcdf'], + 'ncx' => ['application/x-dtbncx+xml'], + 'nds' => ['application/x-nintendo-ds-rom'], + 'nef' => ['image/x-nikon-nef'], + 'nes' => ['application/x-nes-rom'], + 'nez' => ['application/x-nes-rom'], + 'nfo' => ['text/x-nfo'], + 'ngc' => ['application/x-neo-geo-pocket-color-rom'], + 'ngdat' => ['application/vnd.nokia.n-gage.data'], + 'ngp' => ['application/x-neo-geo-pocket-rom'], + 'nitf' => ['application/vnd.nitf'], + 'nlu' => ['application/vnd.neurolanguage.nlu'], + 'nml' => ['application/vnd.enliven'], + 'nnd' => ['application/vnd.noblenet-directory'], + 'nns' => ['application/vnd.noblenet-sealer'], + 'nnw' => ['application/vnd.noblenet-web'], + 'not' => ['text/x-mup'], + 'npx' => ['image/vnd.net-fpx'], + 'nq' => ['application/n-quads'], + 'nrw' => ['image/x-nikon-nrw'], + 'nsc' => ['application/x-conference', 'application/x-netshow-channel'], + 'nsf' => ['application/vnd.lotus-notes'], + 'nsv' => ['video/x-nsv'], + 'nt' => ['application/n-triples'], + 'ntf' => ['application/vnd.nitf'], + 'numbers' => ['application/vnd.apple.numbers', 'application/x-iwork-numbers-sffnumbers'], + 'nzb' => ['application/x-nzb'], + 'o' => ['application/x-object'], + 'oa2' => ['application/vnd.fujitsu.oasys2'], + 'oa3' => ['application/vnd.fujitsu.oasys3'], + 'oas' => ['application/vnd.fujitsu.oasys'], + 'obd' => ['application/x-msbinder'], + 'obgx' => ['application/vnd.openblox.game+xml'], + 'obj' => ['application/x-tgif', 'model/obj'], + 'ocl' => ['text/x-ocl'], + 'oda' => ['application/oda'], + 'odb' => ['application/vnd.oasis.opendocument.database', 'application/vnd.sun.xml.base'], + 'odc' => ['application/vnd.oasis.opendocument.chart'], + 'odf' => ['application/vnd.oasis.opendocument.formula'], + 'odft' => ['application/vnd.oasis.opendocument.formula-template'], + 'odg' => ['application/vnd.oasis.opendocument.graphics'], + 'odi' => ['application/vnd.oasis.opendocument.image'], + 'odm' => ['application/vnd.oasis.opendocument.text-master'], + 'odp' => ['application/vnd.oasis.opendocument.presentation'], + 'ods' => ['application/vnd.oasis.opendocument.spreadsheet'], + 'odt' => ['application/vnd.oasis.opendocument.text'], + 'oga' => ['audio/ogg', 'audio/vorbis', 'audio/x-flac+ogg', 'audio/x-ogg', 'audio/x-oggflac', 'audio/x-speex+ogg', 'audio/x-vorbis', 'audio/x-vorbis+ogg'], + 'ogex' => ['model/vnd.opengex'], + 'ogg' => ['audio/ogg', 'audio/vorbis', 'audio/x-flac+ogg', 'audio/x-ogg', 'audio/x-oggflac', 'audio/x-speex+ogg', 'audio/x-vorbis', 'audio/x-vorbis+ogg', 'video/ogg', 'video/x-ogg', 'video/x-theora', 'video/x-theora+ogg'], + 'ogm' => ['video/x-ogm', 'video/x-ogm+ogg'], + 'ogv' => ['video/ogg', 'video/x-ogg'], + 'ogx' => ['application/ogg', 'application/x-ogg'], + 'old' => ['application/x-trash'], + 'oleo' => ['application/x-oleo'], + 'omdoc' => ['application/omdoc+xml'], + 'onepkg' => ['application/onenote'], + 'onetmp' => ['application/onenote'], + 'onetoc' => ['application/onenote'], + 'onetoc2' => ['application/onenote'], + 'ooc' => ['text/x-ooc'], + 'opf' => ['application/oebps-package+xml'], + 'opml' => ['text/x-opml', 'text/x-opml+xml'], + 'oprc' => ['application/vnd.palm', 'application/x-palm-database'], + 'opus' => ['audio/ogg', 'audio/x-ogg', 'audio/x-opus+ogg'], + 'ora' => ['image/openraster'], + 'orf' => ['image/x-olympus-orf'], + 'org' => ['application/vnd.lotus-organizer', 'text/org', 'text/x-org'], + 'osf' => ['application/vnd.yamaha.openscoreformat'], + 'osfpvg' => ['application/vnd.yamaha.openscoreformat.osfpvg+xml'], + 'osm' => ['application/vnd.openstreetmap.data+xml'], + 'otc' => ['application/vnd.oasis.opendocument.chart-template'], + 'otf' => ['application/vnd.oasis.opendocument.formula-template', 'application/x-font-otf', 'font/otf'], + 'otg' => ['application/vnd.oasis.opendocument.graphics-template'], + 'oth' => ['application/vnd.oasis.opendocument.text-web'], + 'oti' => ['application/vnd.oasis.opendocument.image-template'], + 'otp' => ['application/vnd.oasis.opendocument.presentation-template'], + 'ots' => ['application/vnd.oasis.opendocument.spreadsheet-template'], + 'ott' => ['application/vnd.oasis.opendocument.text-template'], + 'ova' => ['application/ovf', 'application/x-virtualbox-ova'], + 'ovf' => ['application/x-virtualbox-ovf'], + 'owl' => ['application/rdf+xml', 'text/rdf'], + 'owx' => ['application/owl+xml'], + 'oxps' => ['application/oxps'], + 'oxt' => ['application/vnd.openofficeorg.extension'], + 'p' => ['text/x-pascal'], + 'p10' => ['application/pkcs10'], + 'p12' => ['application/pkcs12', 'application/x-pkcs12'], + 'p65' => ['application/x-pagemaker'], + 'p7b' => ['application/x-pkcs7-certificates'], + 'p7c' => ['application/pkcs7-mime'], + 'p7m' => ['application/pkcs7-mime'], + 'p7r' => ['application/x-pkcs7-certreqresp'], + 'p7s' => ['application/pkcs7-signature'], + 'p8' => ['application/pkcs8'], + 'p8e' => ['application/pkcs8-encrypted'], + 'pac' => ['application/x-ns-proxy-autoconfig'], + 'pack' => ['application/x-java-pack200'], + 'pages' => ['application/vnd.apple.pages', 'application/x-iwork-pages-sffpages'], + 'pak' => ['application/x-pak'], + 'par2' => ['application/x-par2'], + 'part' => ['application/x-partial-download'], + 'pas' => ['text/x-pascal'], + 'pat' => ['image/x-gimp-pat'], + 'patch' => ['text/x-diff', 'text/x-patch'], + 'path' => ['text/x-systemd-unit'], + 'paw' => ['application/vnd.pawaafile'], + 'pbd' => ['application/vnd.powerbuilder6'], + 'pbm' => ['image/x-portable-bitmap'], + 'pcap' => ['application/pcap', 'application/vnd.tcpdump.pcap', 'application/x-pcap'], + 'pcd' => ['image/x-photo-cd'], + 'pce' => ['application/x-pc-engine-rom'], + 'pcf' => ['application/x-cisco-vpn-settings', 'application/x-font-pcf'], + 'pcf.Z' => ['application/x-font-pcf'], + 'pcf.gz' => ['application/x-font-pcf'], + 'pcl' => ['application/vnd.hp-pcl'], + 'pclxl' => ['application/vnd.hp-pclxl'], + 'pct' => ['image/x-pict'], + 'pcurl' => ['application/vnd.curl.pcurl'], + 'pcx' => ['image/vnd.zbrush.pcx', 'image/x-pcx'], + 'pdb' => ['application/vnd.palm', 'application/x-aportisdoc', 'application/x-palm-database', 'application/x-pilot'], + 'pdc' => ['application/x-aportisdoc'], + 'pde' => ['text/x-processing'], + 'pdf' => ['application/pdf', 'application/acrobat', 'application/nappdf', 'application/x-pdf', 'image/pdf'], + 'pdf.bz2' => ['application/x-bzpdf'], + 'pdf.gz' => ['application/x-gzpdf'], + 'pdf.lz' => ['application/x-lzpdf'], + 'pdf.xz' => ['application/x-xzpdf'], + 'pef' => ['image/x-pentax-pef'], + 'pem' => ['application/x-x509-ca-cert'], + 'perl' => ['application/x-perl', 'text/x-perl'], + 'pfa' => ['application/x-font-type1'], + 'pfb' => ['application/x-font-type1'], + 'pfm' => ['application/x-font-type1'], + 'pfr' => ['application/font-tdpfr'], + 'pfx' => ['application/pkcs12', 'application/x-pkcs12'], + 'pgm' => ['image/x-portable-graymap'], + 'pgn' => ['application/vnd.chess-pgn', 'application/x-chess-pgn'], + 'pgp' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature'], + 'php' => ['application/x-php', 'application/x-httpd-php'], + 'php3' => ['application/x-php'], + 'php4' => ['application/x-php'], + 'php5' => ['application/x-php'], + 'phps' => ['application/x-php'], + 'pic' => ['image/x-pict'], + 'pict' => ['image/x-pict'], + 'pict1' => ['image/x-pict'], + 'pict2' => ['image/x-pict'], + 'pk' => ['application/x-tex-pk'], + 'pkg' => ['application/x-xar'], + 'pki' => ['application/pkixcmp'], + 'pkipath' => ['application/pkix-pkipath'], + 'pkpass' => ['application/vnd.apple.pkpass'], + 'pkr' => ['application/pgp-keys'], + 'pl' => ['application/x-perl', 'text/x-perl'], + 'pla' => ['audio/x-iriver-pla'], + 'plb' => ['application/vnd.3gpp.pic-bw-large'], + 'plc' => ['application/vnd.mobius.plc'], + 'plf' => ['application/vnd.pocketlearn'], + 'pln' => ['application/x-planperfect'], + 'pls' => ['application/pls', 'application/pls+xml', 'audio/scpls', 'audio/x-scpls'], + 'pm' => ['application/x-pagemaker', 'application/x-perl', 'text/x-perl'], + 'pm6' => ['application/x-pagemaker'], + 'pmd' => ['application/x-pagemaker'], + 'pml' => ['application/vnd.ctc-posml'], + 'png' => ['image/png'], + 'pnm' => ['image/x-portable-anymap'], + 'pntg' => ['image/x-macpaint'], + 'po' => ['application/x-gettext', 'text/x-gettext-translation', 'text/x-po'], + 'pod' => ['application/x-perl', 'text/x-perl'], + 'por' => ['application/x-spss-por'], + 'portpkg' => ['application/vnd.macports.portpkg'], + 'pot' => ['application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint', 'text/x-gettext-translation-template', 'text/x-pot'], + 'potm' => ['application/vnd.ms-powerpoint.template.macroenabled.12'], + 'potx' => ['application/vnd.openxmlformats-officedocument.presentationml.template'], + 'ppam' => ['application/vnd.ms-powerpoint.addin.macroenabled.12'], + 'ppd' => ['application/vnd.cups-ppd'], + 'ppm' => ['image/x-portable-pixmap'], + 'pps' => ['application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint'], + 'ppsm' => ['application/vnd.ms-powerpoint.slideshow.macroenabled.12'], + 'ppsx' => ['application/vnd.openxmlformats-officedocument.presentationml.slideshow'], + 'ppt' => ['application/vnd.ms-powerpoint', 'application/mspowerpoint', 'application/powerpoint', 'application/x-mspowerpoint'], + 'pptm' => ['application/vnd.ms-powerpoint.presentation.macroenabled.12'], + 'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'], + 'ppz' => ['application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint'], + 'pqa' => ['application/vnd.palm', 'application/x-palm-database'], + 'prc' => ['application/vnd.palm', 'application/x-mobipocket-ebook', 'application/x-palm-database', 'application/x-pilot'], + 'pre' => ['application/vnd.lotus-freelance'], + 'prf' => ['application/pics-rules'], + 'provx' => ['application/provenance+xml'], + 'ps' => ['application/postscript'], + 'ps.bz2' => ['application/x-bzpostscript'], + 'ps.gz' => ['application/x-gzpostscript'], + 'psb' => ['application/vnd.3gpp.pic-bw-small'], + 'psd' => ['application/photoshop', 'application/x-photoshop', 'image/photoshop', 'image/psd', 'image/vnd.adobe.photoshop', 'image/x-photoshop', 'image/x-psd'], + 'psf' => ['application/x-font-linux-psf', 'audio/x-psf'], + 'psf.gz' => ['application/x-gz-font-linux-psf'], + 'psflib' => ['audio/x-psflib'], + 'psid' => ['audio/prs.sid'], + 'pskcxml' => ['application/pskc+xml'], + 'psw' => ['application/x-pocket-word'], + 'pti' => ['image/prs.pti'], + 'ptid' => ['application/vnd.pvi.ptid1'], + 'pub' => ['application/vnd.ms-publisher', 'application/x-mspublisher'], + 'pvb' => ['application/vnd.3gpp.pic-bw-var'], + 'pw' => ['application/x-pw'], + 'pwn' => ['application/vnd.3m.post-it-notes'], + 'py' => ['text/x-python', 'text/x-python3'], + 'py3' => ['text/x-python3'], + 'py3x' => ['text/x-python3'], + 'pya' => ['audio/vnd.ms-playready.media.pya'], + 'pyc' => ['application/x-python-bytecode'], + 'pyi' => ['text/x-python3'], + 'pyo' => ['application/x-python-bytecode'], + 'pys' => ['application/x-pyspread-bz-spreadsheet'], + 'pysu' => ['application/x-pyspread-spreadsheet'], + 'pyv' => ['video/vnd.ms-playready.media.pyv'], + 'pyx' => ['text/x-python'], + 'qam' => ['application/vnd.epson.quickanime'], + 'qbo' => ['application/vnd.intu.qbo'], + 'qcow' => ['application/x-qemu-disk'], + 'qcow2' => ['application/x-qemu-disk'], + 'qd' => ['application/x-fd-file', 'application/x-raw-floppy-disk-image'], + 'qed' => ['application/x-qed-disk'], + 'qfx' => ['application/vnd.intu.qfx'], + 'qif' => ['application/x-qw', 'image/x-quicktime'], + 'qml' => ['text/x-qml'], + 'qmlproject' => ['text/x-qml'], + 'qmltypes' => ['text/x-qml'], + 'qp' => ['application/x-qpress'], + 'qps' => ['application/vnd.publishare-delta-tree'], + 'qt' => ['video/quicktime'], + 'qti' => ['application/x-qtiplot'], + 'qti.gz' => ['application/x-qtiplot'], + 'qtif' => ['image/x-quicktime'], + 'qtl' => ['application/x-quicktime-media-link', 'application/x-quicktimeplayer'], + 'qtvr' => ['video/quicktime'], + 'qwd' => ['application/vnd.quark.quarkxpress'], + 'qwt' => ['application/vnd.quark.quarkxpress'], + 'qxb' => ['application/vnd.quark.quarkxpress'], + 'qxd' => ['application/vnd.quark.quarkxpress'], + 'qxl' => ['application/vnd.quark.quarkxpress'], + 'qxt' => ['application/vnd.quark.quarkxpress'], + 'ra' => ['audio/vnd.m-realaudio', 'audio/vnd.rn-realaudio', 'audio/x-pn-realaudio', 'audio/x-realaudio'], + 'raf' => ['image/x-fuji-raf'], + 'ram' => ['application/ram', 'audio/x-pn-realaudio'], + 'raml' => ['application/raml+yaml'], + 'rapd' => ['application/route-apd+xml'], + 'rar' => ['application/x-rar-compressed', 'application/vnd.rar', 'application/x-rar'], + 'ras' => ['image/x-cmu-raster'], + 'raw' => ['image/x-panasonic-raw', 'image/x-panasonic-rw'], + 'raw-disk-image' => ['application/x-raw-disk-image'], + 'raw-disk-image.xz' => ['application/x-raw-disk-image-xz-compressed'], + 'rax' => ['audio/vnd.m-realaudio', 'audio/vnd.rn-realaudio', 'audio/x-pn-realaudio'], + 'rb' => ['application/x-ruby'], + 'rcprofile' => ['application/vnd.ipunplugged.rcprofile'], + 'rdf' => ['application/rdf+xml', 'text/rdf'], + 'rdfs' => ['application/rdf+xml', 'text/rdf'], + 'rdz' => ['application/vnd.data-vision.rdz'], + 'reg' => ['text/x-ms-regedit'], + 'rej' => ['application/x-reject', 'text/x-reject'], + 'relo' => ['application/p2p-overlay+xml'], + 'rep' => ['application/vnd.businessobjects'], + 'res' => ['application/x-dtbresource+xml'], + 'rgb' => ['image/x-rgb'], + 'rif' => ['application/reginfo+xml'], + 'rip' => ['audio/vnd.rip'], + 'ris' => ['application/x-research-info-systems'], + 'rl' => ['application/resource-lists+xml'], + 'rlc' => ['image/vnd.fujixerox.edmics-rlc'], + 'rld' => ['application/resource-lists-diff+xml'], + 'rle' => ['image/rle'], + 'rm' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmi' => ['audio/midi'], + 'rmj' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmm' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmp' => ['audio/x-pn-realaudio-plugin'], + 'rms' => ['application/vnd.jcp.javame.midlet-rms', 'application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmvb' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmx' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rnc' => ['application/relax-ng-compact-syntax', 'application/x-rnc'], + 'rng' => ['application/xml', 'text/xml'], + 'roa' => ['application/rpki-roa'], + 'roff' => ['application/x-troff', 'text/troff', 'text/x-troff'], + 'ros' => ['text/x-common-lisp'], + 'rp' => ['image/vnd.rn-realpix'], + 'rp9' => ['application/vnd.cloanto.rp9'], + 'rpm' => ['application/x-redhat-package-manager', 'application/x-rpm'], + 'rpss' => ['application/vnd.nokia.radio-presets'], + 'rpst' => ['application/vnd.nokia.radio-preset'], + 'rq' => ['application/sparql-query'], + 'rs' => ['application/rls-services+xml', 'text/rust'], + 'rsat' => ['application/atsc-rsat+xml'], + 'rsd' => ['application/rsd+xml'], + 'rsheet' => ['application/urc-ressheet+xml'], + 'rss' => ['application/rss+xml', 'text/rss'], + 'rst' => ['text/x-rst'], + 'rt' => ['text/vnd.rn-realtext'], + 'rtf' => ['application/rtf', 'text/rtf'], + 'rtx' => ['text/richtext'], + 'run' => ['application/x-makeself'], + 'rusd' => ['application/route-usd+xml'], + 'rv' => ['video/vnd.rn-realvideo', 'video/x-real-video'], + 'rvx' => ['video/vnd.rn-realvideo', 'video/x-real-video'], + 'rw2' => ['image/x-panasonic-raw2', 'image/x-panasonic-rw2'], + 's' => ['text/x-asm'], + 's3m' => ['audio/s3m', 'audio/x-s3m'], + 'saf' => ['application/vnd.yamaha.smaf-audio'], + 'sage' => ['text/x-sagemath'], + 'sam' => ['application/x-amipro'], + 'sami' => ['application/x-sami'], + 'sap' => ['application/x-sap-file', 'application/x-thomson-sap-image'], + 'sass' => ['text/x-sass'], + 'sav' => ['application/x-spss-sav', 'application/x-spss-savefile'], + 'sbml' => ['application/sbml+xml'], + 'sc' => ['application/vnd.ibm.secure-container', 'text/x-scala'], + 'scala' => ['text/x-scala'], + 'scd' => ['application/x-msschedule'], + 'scm' => ['application/vnd.lotus-screencam', 'text/x-scheme'], + 'scope' => ['text/x-systemd-unit'], + 'scq' => ['application/scvp-cv-request'], + 'scs' => ['application/scvp-cv-response'], + 'scss' => ['text/x-scss'], + 'scurl' => ['text/vnd.curl.scurl'], + 'sda' => ['application/vnd.stardivision.draw'], + 'sdc' => ['application/vnd.stardivision.calc'], + 'sdd' => ['application/vnd.stardivision.impress'], + 'sdkd' => ['application/vnd.solent.sdkm+xml'], + 'sdkm' => ['application/vnd.solent.sdkm+xml'], + 'sdp' => ['application/sdp', 'application/vnd.sdp', 'application/vnd.stardivision.impress', 'application/x-sdp'], + 'sds' => ['application/vnd.stardivision.chart'], + 'sdw' => ['application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global'], + 'sea' => ['application/x-sea'], + 'see' => ['application/vnd.seemail'], + 'seed' => ['application/vnd.fdsn.seed'], + 'sema' => ['application/vnd.sema'], + 'semd' => ['application/vnd.semd'], + 'semf' => ['application/vnd.semf'], + 'senmlx' => ['application/senml+xml'], + 'sensmlx' => ['application/sensml+xml'], + 'ser' => ['application/java-serialized-object'], + 'service' => ['text/x-dbus-service', 'text/x-systemd-unit'], + 'setpay' => ['application/set-payment-initiation'], + 'setreg' => ['application/set-registration-initiation'], + 'sfc' => ['application/vnd.nintendo.snes.rom', 'application/x-snes-rom'], + 'sfd-hdstx' => ['application/vnd.hydrostatix.sof-data'], + 'sfs' => ['application/vnd.spotfire.sfs'], + 'sfv' => ['text/x-sfv'], + 'sg' => ['application/x-sg1000-rom'], + 'sgb' => ['application/x-gameboy-rom'], + 'sgd' => ['application/x-genesis-rom'], + 'sgf' => ['application/x-go-sgf'], + 'sgi' => ['image/sgi', 'image/x-sgi'], + 'sgl' => ['application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global'], + 'sgm' => ['text/sgml'], + 'sgml' => ['text/sgml'], + 'sh' => ['application/x-sh', 'application/x-shellscript', 'text/x-sh'], + 'shape' => ['application/x-dia-shape'], + 'shar' => ['application/x-shar'], + 'shex' => ['text/shex'], + 'shf' => ['application/shf+xml'], + 'shn' => ['application/x-shorten', 'audio/x-shorten'], + 'shtml' => ['text/html'], + 'siag' => ['application/x-siag'], + 'sid' => ['audio/prs.sid', 'image/x-mrsid-image'], + 'sieve' => ['application/sieve'], + 'sig' => ['application/pgp-signature'], + 'sik' => ['application/x-trash'], + 'sil' => ['audio/silk'], + 'silo' => ['model/mesh'], + 'sis' => ['application/vnd.symbian.install'], + 'sisx' => ['application/vnd.symbian.install', 'x-epoc/x-sisx-app'], + 'sit' => ['application/x-stuffit', 'application/stuffit', 'application/x-sit'], + 'sitx' => ['application/x-stuffitx'], + 'siv' => ['application/sieve'], + 'sk' => ['image/x-skencil'], + 'sk1' => ['image/x-skencil'], + 'skd' => ['application/vnd.koan'], + 'skm' => ['application/vnd.koan'], + 'skp' => ['application/vnd.koan'], + 'skr' => ['application/pgp-keys'], + 'skt' => ['application/vnd.koan'], + 'sldm' => ['application/vnd.ms-powerpoint.slide.macroenabled.12'], + 'sldx' => ['application/vnd.openxmlformats-officedocument.presentationml.slide'], + 'slice' => ['text/x-systemd-unit'], + 'slim' => ['text/slim'], + 'slk' => ['text/spreadsheet'], + 'slm' => ['text/slim'], + 'sls' => ['application/route-s-tsid+xml'], + 'slt' => ['application/vnd.epson.salt'], + 'sm' => ['application/vnd.stepmania.stepchart'], + 'smaf' => ['application/vnd.smaf', 'application/x-smaf'], + 'smc' => ['application/vnd.nintendo.snes.rom', 'application/x-snes-rom'], + 'smd' => ['application/vnd.stardivision.mail', 'application/x-genesis-rom'], + 'smf' => ['application/vnd.stardivision.math'], + 'smi' => ['application/smil', 'application/smil+xml', 'application/x-sami'], + 'smil' => ['application/smil', 'application/smil+xml'], + 'smk' => ['video/vnd.radgamettools.smacker'], + 'sml' => ['application/smil', 'application/smil+xml'], + 'sms' => ['application/x-sms-rom'], + 'smv' => ['video/x-smv'], + 'smzip' => ['application/vnd.stepmania.package'], + 'snap' => ['application/vnd.snap'], + 'snd' => ['audio/basic'], + 'snf' => ['application/x-font-snf'], + 'so' => ['application/x-sharedlib'], + 'socket' => ['text/x-systemd-unit'], + 'spc' => ['application/x-pkcs7-certificates'], + 'spd' => ['application/x-font-speedo'], + 'spdx' => ['text/spdx'], + 'spec' => ['text/x-rpm-spec'], + 'spf' => ['application/vnd.yamaha.smaf-phrase'], + 'spl' => ['application/futuresplash', 'application/vnd.adobe.flash.movie', 'application/x-futuresplash', 'application/x-shockwave-flash'], + 'spm' => ['application/x-source-rpm'], + 'spot' => ['text/vnd.in3d.spot'], + 'spp' => ['application/scvp-vp-response'], + 'spq' => ['application/scvp-vp-request'], + 'spx' => ['application/x-apple-systemprofiler+xml', 'audio/ogg', 'audio/x-speex', 'audio/x-speex+ogg'], + 'sql' => ['application/sql', 'application/x-sql', 'text/x-sql'], + 'sqlite2' => ['application/x-sqlite2'], + 'sqlite3' => ['application/vnd.sqlite3', 'application/x-sqlite3'], + 'sqsh' => ['application/vnd.squashfs'], + 'sr2' => ['image/x-sony-sr2'], + 'src' => ['application/x-wais-source'], + 'src.rpm' => ['application/x-source-rpm'], + 'srf' => ['image/x-sony-srf'], + 'srt' => ['application/x-srt', 'application/x-subrip'], + 'sru' => ['application/sru+xml'], + 'srx' => ['application/sparql-results+xml'], + 'ss' => ['text/x-scheme'], + 'ssa' => ['text/x-ssa'], + 'ssdl' => ['application/ssdl+xml'], + 'sse' => ['application/vnd.kodak-descriptor'], + 'ssf' => ['application/vnd.epson.ssf'], + 'ssml' => ['application/ssml+xml'], + 'st' => ['application/vnd.sailingtracker.track'], + 'stc' => ['application/vnd.sun.xml.calc.template'], + 'std' => ['application/vnd.sun.xml.draw.template'], + 'stf' => ['application/vnd.wt.stf'], + 'sti' => ['application/vnd.sun.xml.impress.template'], + 'stk' => ['application/hyperstudio'], + 'stl' => ['application/vnd.ms-pki.stl', 'model/stl', 'model/x.stl-ascii', 'model/x.stl-binary'], + 'stm' => ['audio/x-stm'], + 'stpxz' => ['model/step-xml+zip'], + 'stpz' => ['model/step+zip'], + 'str' => ['application/vnd.pg.format'], + 'stw' => ['application/vnd.sun.xml.writer.template'], + 'sty' => ['application/x-tex', 'text/x-tex'], + 'styl' => ['text/stylus'], + 'stylus' => ['text/stylus'], + 'sub' => ['image/vnd.dvb.subtitle', 'text/vnd.dvb.subtitle', 'text/x-microdvd', 'text/x-mpsub', 'text/x-subviewer'], + 'sun' => ['image/x-sun-raster'], + 'sus' => ['application/vnd.sus-calendar'], + 'susp' => ['application/vnd.sus-calendar'], + 'sv' => ['text/x-svsrc'], + 'sv4cpio' => ['application/x-sv4cpio'], + 'sv4crc' => ['application/x-sv4crc'], + 'svc' => ['application/vnd.dvb.service'], + 'svd' => ['application/vnd.svd'], + 'svg' => ['image/svg+xml', 'image/svg'], + 'svgz' => ['image/svg+xml', 'image/svg+xml-compressed'], + 'svh' => ['text/x-svhdr'], + 'swa' => ['application/x-director'], + 'swap' => ['text/x-systemd-unit'], + 'swf' => ['application/futuresplash', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash'], + 'swi' => ['application/vnd.aristanetworks.swi'], + 'swidtag' => ['application/swid+xml'], + 'swm' => ['application/x-ms-wim'], + 'sxc' => ['application/vnd.sun.xml.calc'], + 'sxd' => ['application/vnd.sun.xml.draw'], + 'sxg' => ['application/vnd.sun.xml.writer.global'], + 'sxi' => ['application/vnd.sun.xml.impress'], + 'sxm' => ['application/vnd.sun.xml.math'], + 'sxw' => ['application/vnd.sun.xml.writer'], + 'sylk' => ['text/spreadsheet'], + 't' => ['application/x-perl', 'application/x-troff', 'text/troff', 'text/x-perl', 'text/x-troff'], + 't2t' => ['text/x-txt2tags'], + 't3' => ['application/x-t3vm-image'], + 't38' => ['image/t38'], + 'taglet' => ['application/vnd.mynfc'], + 'tao' => ['application/vnd.tao.intent-module-archive'], + 'tap' => ['image/vnd.tencent.tap'], + 'tar' => ['application/x-tar', 'application/x-gtar'], + 'tar.Z' => ['application/x-tarz'], + 'tar.bz' => ['application/x-bzip-compressed-tar'], + 'tar.bz2' => ['application/x-bzip-compressed-tar'], + 'tar.gz' => ['application/x-compressed-tar'], + 'tar.lrz' => ['application/x-lrzip-compressed-tar'], + 'tar.lz' => ['application/x-lzip-compressed-tar'], + 'tar.lz4' => ['application/x-lz4-compressed-tar'], + 'tar.lzma' => ['application/x-lzma-compressed-tar'], + 'tar.lzo' => ['application/x-tzo'], + 'tar.xz' => ['application/x-xz-compressed-tar'], + 'tar.zst' => ['application/x-zstd-compressed-tar'], + 'target' => ['text/x-systemd-unit'], + 'taz' => ['application/x-tarz'], + 'tb2' => ['application/x-bzip-compressed-tar'], + 'tbz' => ['application/x-bzip-compressed-tar'], + 'tbz2' => ['application/x-bzip-compressed-tar'], + 'tcap' => ['application/vnd.3gpp2.tcap'], + 'tcl' => ['application/x-tcl', 'text/tcl', 'text/x-tcl'], + 'td' => ['application/urc-targetdesc+xml'], + 'teacher' => ['application/vnd.smart.teacher'], + 'tei' => ['application/tei+xml'], + 'teicorpus' => ['application/tei+xml'], + 'tex' => ['application/x-tex', 'text/x-tex'], + 'texi' => ['application/x-texinfo', 'text/x-texinfo'], + 'texinfo' => ['application/x-texinfo', 'text/x-texinfo'], + 'text' => ['text/plain'], + 'tfi' => ['application/thraud+xml'], + 'tfm' => ['application/x-tex-tfm'], + 'tfx' => ['image/tiff-fx'], + 'tga' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'tgz' => ['application/x-compressed-tar'], + 'theme' => ['application/x-theme'], + 'themepack' => ['application/x-windows-themepack'], + 'thmx' => ['application/vnd.ms-officetheme'], + 'tif' => ['image/tiff'], + 'tiff' => ['image/tiff'], + 'timer' => ['text/x-systemd-unit'], + 'tk' => ['application/x-tcl', 'text/tcl', 'text/x-tcl'], + 'tlrz' => ['application/x-lrzip-compressed-tar'], + 'tlz' => ['application/x-lzma-compressed-tar'], + 'tmo' => ['application/vnd.tmobile-livetv'], + 'tnef' => ['application/ms-tnef', 'application/vnd.ms-tnef'], + 'tnf' => ['application/ms-tnef', 'application/vnd.ms-tnef'], + 'toc' => ['application/x-cdrdao-toc'], + 'toml' => ['application/toml'], + 'torrent' => ['application/x-bittorrent'], + 'tpic' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'tpl' => ['application/vnd.groove-tool-template'], + 'tpt' => ['application/vnd.trid.tpt'], + 'tr' => ['application/x-troff', 'text/troff', 'text/x-troff'], + 'tra' => ['application/vnd.trueapp'], + 'trig' => ['application/trig', 'application/x-trig'], + 'trm' => ['application/x-msterminal'], + 'ts' => ['application/x-linguist', 'text/vnd.qt.linguist', 'text/vnd.trolltech.linguist', 'video/mp2t'], + 'tsd' => ['application/timestamped-data'], + 'tsv' => ['text/tab-separated-values'], + 'tta' => ['audio/tta', 'audio/x-tta'], + 'ttc' => ['font/collection'], + 'ttf' => ['application/x-font-truetype', 'application/x-font-ttf', 'font/ttf'], + 'ttl' => ['text/turtle'], + 'ttml' => ['application/ttml+xml'], + 'ttx' => ['application/x-font-ttx'], + 'twd' => ['application/vnd.simtech-mindmapper'], + 'twds' => ['application/vnd.simtech-mindmapper'], + 'twig' => ['text/x-twig'], + 'txd' => ['application/vnd.genomatix.tuxedo'], + 'txf' => ['application/vnd.mobius.txf'], + 'txt' => ['text/plain'], + 'txz' => ['application/x-xz-compressed-tar'], + 'tzo' => ['application/x-tzo'], + 'tzst' => ['application/x-zstd-compressed-tar'], + 'u32' => ['application/x-authorware-bin'], + 'u8dsn' => ['message/global-delivery-status'], + 'u8hdr' => ['message/global-headers'], + 'u8mdn' => ['message/global-disposition-notification'], + 'u8msg' => ['message/global'], + 'ubj' => ['application/ubjson'], + 'udeb' => ['application/vnd.debian.binary-package', 'application/x-deb', 'application/x-debian-package'], + 'ufd' => ['application/vnd.ufdl'], + 'ufdl' => ['application/vnd.ufdl'], + 'ufraw' => ['application/x-ufraw'], + 'ui' => ['application/x-designer', 'application/x-gtk-builder'], + 'uil' => ['text/x-uil'], + 'ult' => ['audio/x-mod'], + 'ulx' => ['application/x-glulx'], + 'umj' => ['application/vnd.umajin'], + 'unf' => ['application/x-nes-rom'], + 'uni' => ['audio/x-mod'], + 'unif' => ['application/x-nes-rom'], + 'unityweb' => ['application/vnd.unity'], + 'uoml' => ['application/vnd.uoml+xml'], + 'uri' => ['text/uri-list'], + 'uris' => ['text/uri-list'], + 'url' => ['application/x-mswinurl'], + 'urls' => ['text/uri-list'], + 'usdz' => ['model/vnd.usdz+zip'], + 'ustar' => ['application/x-ustar'], + 'utz' => ['application/vnd.uiq.theme'], + 'uu' => ['text/x-uuencode'], + 'uue' => ['text/x-uuencode', 'zz-application/zz-winassoc-uu'], + 'uva' => ['audio/vnd.dece.audio'], + 'uvd' => ['application/vnd.dece.data'], + 'uvf' => ['application/vnd.dece.data'], + 'uvg' => ['image/vnd.dece.graphic'], + 'uvh' => ['video/vnd.dece.hd'], + 'uvi' => ['image/vnd.dece.graphic'], + 'uvm' => ['video/vnd.dece.mobile'], + 'uvp' => ['video/vnd.dece.pd'], + 'uvs' => ['video/vnd.dece.sd'], + 'uvt' => ['application/vnd.dece.ttml+xml'], + 'uvu' => ['video/vnd.uvvu.mp4'], + 'uvv' => ['video/vnd.dece.video'], + 'uvva' => ['audio/vnd.dece.audio'], + 'uvvd' => ['application/vnd.dece.data'], + 'uvvf' => ['application/vnd.dece.data'], + 'uvvg' => ['image/vnd.dece.graphic'], + 'uvvh' => ['video/vnd.dece.hd'], + 'uvvi' => ['image/vnd.dece.graphic'], + 'uvvm' => ['video/vnd.dece.mobile'], + 'uvvp' => ['video/vnd.dece.pd'], + 'uvvs' => ['video/vnd.dece.sd'], + 'uvvt' => ['application/vnd.dece.ttml+xml'], + 'uvvu' => ['video/vnd.uvvu.mp4'], + 'uvvv' => ['video/vnd.dece.video'], + 'uvvx' => ['application/vnd.dece.unspecified'], + 'uvvz' => ['application/vnd.dece.zip'], + 'uvx' => ['application/vnd.dece.unspecified'], + 'uvz' => ['application/vnd.dece.zip'], + 'v' => ['text/x-verilog'], + 'v64' => ['application/x-n64-rom'], + 'vala' => ['text/x-vala'], + 'vapi' => ['text/x-vala'], + 'vb' => ['application/x-virtual-boy-rom'], + 'vbox' => ['application/x-virtualbox-vbox'], + 'vbox-extpack' => ['application/x-virtualbox-vbox-extpack'], + 'vbs' => ['text/vbs', 'text/vbscript'], + 'vcard' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'vcd' => ['application/x-cdlink'], + 'vcf' => ['text/x-vcard', 'text/directory', 'text/vcard'], + 'vcg' => ['application/vnd.groove-vcard'], + 'vcs' => ['application/ics', 'text/calendar', 'text/x-vcalendar'], + 'vct' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'vcx' => ['application/vnd.vcx'], + 'vda' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'vdi' => ['application/x-vdi-disk', 'application/x-virtualbox-vdi'], + 'vds' => ['model/vnd.sap.vds'], + 'vhd' => ['application/x-vhd-disk', 'application/x-virtualbox-vhd', 'text/x-vhdl'], + 'vhdl' => ['text/x-vhdl'], + 'vhdx' => ['application/x-vhdx-disk', 'application/x-virtualbox-vhdx'], + 'vis' => ['application/vnd.visionary'], + 'viv' => ['video/vivo', 'video/vnd.vivo'], + 'vivo' => ['video/vivo', 'video/vnd.vivo'], + 'vlc' => ['application/m3u', 'audio/m3u', 'audio/mpegurl', 'audio/x-m3u', 'audio/x-mp3-playlist', 'audio/x-mpegurl'], + 'vmdk' => ['application/x-virtualbox-vmdk', 'application/x-vmdk-disk'], + 'vob' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2', 'video/x-ms-vob'], + 'voc' => ['audio/x-voc'], + 'vor' => ['application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global'], + 'vox' => ['application/x-authorware-bin'], + 'vpc' => ['application/x-vhd-disk', 'application/x-virtualbox-vhd'], + 'vrm' => ['model/vrml'], + 'vrml' => ['model/vrml'], + 'vsd' => ['application/vnd.visio'], + 'vsdm' => ['application/vnd.ms-visio.drawing.macroenabled.main+xml'], + 'vsdx' => ['application/vnd.ms-visio.drawing.main+xml'], + 'vsf' => ['application/vnd.vsf'], + 'vss' => ['application/vnd.visio'], + 'vssm' => ['application/vnd.ms-visio.stencil.macroenabled.main+xml'], + 'vssx' => ['application/vnd.ms-visio.stencil.main+xml'], + 'vst' => ['application/tga', 'application/vnd.visio', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'vstm' => ['application/vnd.ms-visio.template.macroenabled.main+xml'], + 'vstx' => ['application/vnd.ms-visio.template.main+xml'], + 'vsw' => ['application/vnd.visio'], + 'vtf' => ['image/vnd.valve.source.texture'], + 'vtt' => ['text/vtt'], + 'vtu' => ['model/vnd.vtu'], + 'vxml' => ['application/voicexml+xml'], + 'w3d' => ['application/x-director'], + 'wad' => ['application/x-doom', 'application/x-doom-wad', 'application/x-wii-wad'], + 'wadl' => ['application/vnd.sun.wadl+xml'], + 'war' => ['application/java-archive'], + 'wasm' => ['application/wasm'], + 'wav' => ['audio/wav', 'audio/vnd.wave', 'audio/wave', 'audio/x-wav'], + 'wax' => ['application/x-ms-asx', 'audio/x-ms-asx', 'audio/x-ms-wax', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'wb1' => ['application/x-quattropro'], + 'wb2' => ['application/x-quattropro'], + 'wb3' => ['application/x-quattropro'], + 'wbmp' => ['image/vnd.wap.wbmp'], + 'wbs' => ['application/vnd.criticaltools.wbs+xml'], + 'wbxml' => ['application/vnd.wap.wbxml'], + 'wcm' => ['application/vnd.ms-works'], + 'wdb' => ['application/vnd.ms-works'], + 'wdp' => ['image/vnd.ms-photo'], + 'weba' => ['audio/webm'], + 'webapp' => ['application/x-web-app-manifest+json'], + 'webm' => ['video/webm'], + 'webmanifest' => ['application/manifest+json'], + 'webp' => ['image/webp'], + 'wg' => ['application/vnd.pmi.widget'], + 'wgt' => ['application/widget'], + 'wim' => ['application/x-ms-wim'], + 'wk1' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wk3' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wk4' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wkdownload' => ['application/x-partial-download'], + 'wks' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/vnd.ms-works', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wm' => ['video/x-ms-wm'], + 'wma' => ['audio/x-ms-wma', 'audio/wma'], + 'wmd' => ['application/x-ms-wmd'], + 'wmf' => ['application/wmf', 'application/x-msmetafile', 'application/x-wmf', 'image/wmf', 'image/x-win-metafile', 'image/x-wmf'], + 'wml' => ['text/vnd.wap.wml'], + 'wmlc' => ['application/vnd.wap.wmlc'], + 'wmls' => ['text/vnd.wap.wmlscript'], + 'wmlsc' => ['application/vnd.wap.wmlscriptc'], + 'wmv' => ['audio/x-ms-wmv', 'video/x-ms-wmv'], + 'wmx' => ['application/x-ms-asx', 'audio/x-ms-asx', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'wmz' => ['application/x-ms-wmz', 'application/x-msmetafile'], + 'woff' => ['application/font-woff', 'application/x-font-woff', 'font/woff'], + 'woff2' => ['font/woff2'], + 'wp' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wp4' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wp5' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wp6' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wpd' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wpg' => ['application/x-wpg'], + 'wpl' => ['application/vnd.ms-wpl'], + 'wpp' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wps' => ['application/vnd.ms-works'], + 'wqd' => ['application/vnd.wqd'], + 'wri' => ['application/x-mswrite'], + 'wrl' => ['model/vrml'], + 'ws' => ['application/x-wonderswan-rom'], + 'wsc' => ['application/x-wonderswan-color-rom', 'message/vnd.wfa.wsc'], + 'wsdl' => ['application/wsdl+xml'], + 'wsgi' => ['text/x-python'], + 'wspolicy' => ['application/wspolicy+xml'], + 'wtb' => ['application/vnd.webturbo'], + 'wv' => ['audio/x-wavpack'], + 'wvc' => ['audio/x-wavpack-correction'], + 'wvp' => ['audio/x-wavpack'], + 'wvx' => ['application/x-ms-asx', 'audio/x-ms-asx', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'wwf' => ['application/wwf', 'application/x-wwf'], + 'x32' => ['application/x-authorware-bin'], + 'x3d' => ['model/x3d+xml'], + 'x3db' => ['model/x3d+binary', 'model/x3d+fastinfoset'], + 'x3dbz' => ['model/x3d+binary'], + 'x3dv' => ['model/x3d+vrml', 'model/x3d-vrml'], + 'x3dvz' => ['model/x3d+vrml'], + 'x3dz' => ['model/x3d+xml'], + 'x3f' => ['image/x-sigma-x3f'], + 'x_b' => ['model/vnd.parasolid.transmit.binary'], + 'x_t' => ['model/vnd.parasolid.transmit.text'], + 'xac' => ['application/x-gnucash'], + 'xaml' => ['application/xaml+xml'], + 'xap' => ['application/x-silverlight-app'], + 'xar' => ['application/vnd.xara', 'application/x-xar'], + 'xav' => ['application/xcap-att+xml'], + 'xbap' => ['application/x-ms-xbap'], + 'xbd' => ['application/vnd.fujixerox.docuworks.binder'], + 'xbel' => ['application/x-xbel'], + 'xbl' => ['application/xml', 'text/xml'], + 'xbm' => ['image/x-xbitmap'], + 'xca' => ['application/xcap-caps+xml'], + 'xcf' => ['image/x-xcf'], + 'xcf.bz2' => ['image/x-compressed-xcf'], + 'xcf.gz' => ['image/x-compressed-xcf'], + 'xcs' => ['application/calendar+xml'], + 'xdf' => ['application/mrb-consumer+xml', 'application/mrb-publish+xml', 'application/xcap-diff+xml'], + 'xdgapp' => ['application/vnd.flatpak', 'application/vnd.xdgapp'], + 'xdm' => ['application/vnd.syncml.dm+xml'], + 'xdp' => ['application/vnd.adobe.xdp+xml'], + 'xdssc' => ['application/dssc+xml'], + 'xdw' => ['application/vnd.fujixerox.docuworks'], + 'xel' => ['application/xcap-el+xml'], + 'xenc' => ['application/xenc+xml'], + 'xer' => ['application/patch-ops-error+xml', 'application/xcap-error+xml'], + 'xfdf' => ['application/vnd.adobe.xfdf'], + 'xfdl' => ['application/vnd.xfdl'], + 'xhe' => ['audio/usac'], + 'xht' => ['application/xhtml+xml'], + 'xhtml' => ['application/xhtml+xml'], + 'xhvml' => ['application/xv+xml'], + 'xi' => ['audio/x-xi'], + 'xif' => ['image/vnd.xiff'], + 'xla' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlam' => ['application/vnd.ms-excel.addin.macroenabled.12'], + 'xlc' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xld' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlf' => ['application/x-xliff', 'application/x-xliff+xml', 'application/xliff+xml'], + 'xliff' => ['application/x-xliff', 'application/xliff+xml'], + 'xll' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlm' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlr' => ['application/vnd.ms-works'], + 'xls' => ['application/vnd.ms-excel', 'application/msexcel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlsb' => ['application/vnd.ms-excel.sheet.binary.macroenabled.12'], + 'xlsm' => ['application/vnd.ms-excel.sheet.macroenabled.12'], + 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], + 'xlt' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xltm' => ['application/vnd.ms-excel.template.macroenabled.12'], + 'xltx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.template'], + 'xlw' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xm' => ['audio/x-xm', 'audio/xm'], + 'xmf' => ['audio/mobile-xmf', 'audio/x-xmf', 'audio/xmf'], + 'xmi' => ['text/x-xmi'], + 'xml' => ['application/xml', 'text/xml'], + 'xns' => ['application/xcap-ns+xml'], + 'xo' => ['application/vnd.olpc-sugar'], + 'xop' => ['application/xop+xml'], + 'xpi' => ['application/x-xpinstall'], + 'xpl' => ['application/xproc+xml'], + 'xpm' => ['image/x-xpixmap', 'image/x-xpm'], + 'xpr' => ['application/vnd.is-xpr'], + 'xps' => ['application/vnd.ms-xpsdocument', 'application/xps'], + 'xpw' => ['application/vnd.intercon.formnet'], + 'xpx' => ['application/vnd.intercon.formnet'], + 'xsd' => ['application/xml', 'text/xml'], + 'xsl' => ['application/xml', 'application/xslt+xml'], + 'xslfo' => ['text/x-xslfo'], + 'xslt' => ['application/xslt+xml'], + 'xsm' => ['application/vnd.syncml+xml'], + 'xspf' => ['application/x-xspf+xml', 'application/xspf+xml'], + 'xul' => ['application/vnd.mozilla.xul+xml'], + 'xvm' => ['application/xv+xml'], + 'xvml' => ['application/xv+xml'], + 'xwd' => ['image/x-xwindowdump'], + 'xyz' => ['chemical/x-xyz'], + 'xz' => ['application/x-xz'], + 'yaml' => ['application/x-yaml', 'text/x-yaml', 'text/yaml'], + 'yang' => ['application/yang'], + 'yin' => ['application/yin+xml'], + 'yml' => ['application/x-yaml', 'text/x-yaml', 'text/yaml'], + 'ymp' => ['text/x-suse-ymp'], + 'yt' => ['application/vnd.youtube.yt'], + 'z1' => ['application/x-zmachine'], + 'z2' => ['application/x-zmachine'], + 'z3' => ['application/x-zmachine'], + 'z4' => ['application/x-zmachine'], + 'z5' => ['application/x-zmachine'], + 'z6' => ['application/x-zmachine'], + 'z64' => ['application/x-n64-rom'], + 'z7' => ['application/x-zmachine'], + 'z8' => ['application/x-zmachine'], + 'zabw' => ['application/x-abiword'], + 'zaz' => ['application/vnd.zzazz.deck+xml'], + 'zip' => ['application/zip', 'application/x-zip', 'application/x-zip-compressed'], + 'zir' => ['application/vnd.zul'], + 'zirz' => ['application/vnd.zul'], + 'zmm' => ['application/vnd.handheld-entertainment+xml'], + 'zoo' => ['application/x-zoo'], + 'zsav' => ['application/x-spss-sav', 'application/x-spss-savefile'], + 'zst' => ['application/zstd'], + 'zz' => ['application/zlib'], + '123' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + '602' => ['application/x-t602'], + '669' => ['audio/x-mod'], + ]; +} diff --git a/plugins/email/vendor/symfony/mime/MimeTypesInterface.php b/plugins/email/vendor/symfony/mime/MimeTypesInterface.php new file mode 100644 index 0000000..17d45ad --- /dev/null +++ b/plugins/email/vendor/symfony/mime/MimeTypesInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * @author Fabien Potencier + */ +interface MimeTypesInterface extends MimeTypeGuesserInterface +{ + /** + * Gets the extensions for the given MIME type in decreasing order of preference. + * + * @return string[] + */ + public function getExtensions(string $mimeType): array; + + /** + * Gets the MIME types for the given extension in decreasing order of preference. + * + * @return string[] + */ + public function getMimeTypes(string $ext): array; +} diff --git a/plugins/email/vendor/symfony/mime/Part/AbstractMultipartPart.php b/plugins/email/vendor/symfony/mime/Part/AbstractMultipartPart.php new file mode 100644 index 0000000..685d250 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/AbstractMultipartPart.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +abstract class AbstractMultipartPart extends AbstractPart +{ + private $boundary; + private $parts = []; + + public function __construct(AbstractPart ...$parts) + { + parent::__construct(); + + foreach ($parts as $part) { + $this->parts[] = $part; + } + } + + /** + * @return AbstractPart[] + */ + public function getParts(): array + { + return $this->parts; + } + + public function getMediaType(): string + { + return 'multipart'; + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + $headers->setHeaderParameter('Content-Type', 'boundary', $this->getBoundary()); + + return $headers; + } + + public function bodyToString(): string + { + $parts = $this->getParts(); + $string = ''; + foreach ($parts as $part) { + $string .= '--'.$this->getBoundary()."\r\n".$part->toString()."\r\n"; + } + $string .= '--'.$this->getBoundary()."--\r\n"; + + return $string; + } + + public function bodyToIterable(): iterable + { + $parts = $this->getParts(); + foreach ($parts as $part) { + yield '--'.$this->getBoundary()."\r\n"; + yield from $part->toIterable(); + yield "\r\n"; + } + yield '--'.$this->getBoundary()."--\r\n"; + } + + public function asDebugString(): string + { + $str = parent::asDebugString(); + foreach ($this->getParts() as $part) { + $lines = explode("\n", $part->asDebugString()); + $str .= "\n └ ".array_shift($lines); + foreach ($lines as $line) { + $str .= "\n |".$line; + } + } + + return $str; + } + + private function getBoundary(): string + { + if (null === $this->boundary) { + $this->boundary = strtr(base64_encode(random_bytes(6)), '+/', '-_'); + } + + return $this->boundary; + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/AbstractPart.php b/plugins/email/vendor/symfony/mime/Part/AbstractPart.php new file mode 100644 index 0000000..93892d9 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/AbstractPart.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +abstract class AbstractPart +{ + private $headers; + + public function __construct() + { + $this->headers = new Headers(); + } + + public function getHeaders(): Headers + { + return $this->headers; + } + + public function getPreparedHeaders(): Headers + { + $headers = clone $this->headers; + $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype()); + + return $headers; + } + + public function toString(): string + { + return $this->getPreparedHeaders()->toString()."\r\n".$this->bodyToString(); + } + + public function toIterable(): iterable + { + yield $this->getPreparedHeaders()->toString(); + yield "\r\n"; + yield from $this->bodyToIterable(); + } + + public function asDebugString(): string + { + return $this->getMediaType().'/'.$this->getMediaSubtype(); + } + + abstract public function bodyToString(): string; + + abstract public function bodyToIterable(): iterable; + + abstract public function getMediaType(): string; + + abstract public function getMediaSubtype(): string; +} diff --git a/plugins/email/vendor/symfony/mime/Part/DataPart.php b/plugins/email/vendor/symfony/mime/Part/DataPart.php new file mode 100644 index 0000000..4247ce7 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/DataPart.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\MimeTypes; + +/** + * @author Fabien Potencier + */ +class DataPart extends TextPart +{ + /** @internal */ + protected $_parent; + + private static $mimeTypes; + + private $filename; + private $mediaType; + private $cid; + private $handle; + + /** + * @param resource|string $body + */ + public function __construct($body, string $filename = null, string $contentType = null, string $encoding = null) + { + unset($this->_parent); + + if (null === $contentType) { + $contentType = 'application/octet-stream'; + } + [$this->mediaType, $subtype] = explode('/', $contentType); + + parent::__construct($body, null, $subtype, $encoding); + + if (null !== $filename) { + $this->filename = $filename; + $this->setName($filename); + } + $this->setDisposition('attachment'); + } + + public static function fromPath(string $path, string $name = null, string $contentType = null): self + { + if (null === $contentType) { + $ext = strtolower(substr($path, strrpos($path, '.') + 1)); + if (null === self::$mimeTypes) { + self::$mimeTypes = new MimeTypes(); + } + $contentType = self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream'; + } + + if ((is_file($path) && !is_readable($path)) || is_dir($path)) { + throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path)); + } + + if (false === $handle = @fopen($path, 'r', false)) { + throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path)); + } + + if (!is_file($path)) { + $cache = fopen('php://temp', 'r+'); + stream_copy_to_stream($handle, $cache); + $handle = $cache; + } + + $p = new self($handle, $name ?: basename($path), $contentType); + $p->handle = $handle; + + return $p; + } + + /** + * @return $this + */ + public function asInline() + { + return $this->setDisposition('inline'); + } + + public function getContentId(): string + { + return $this->cid ?: $this->cid = $this->generateContentId(); + } + + public function hasContentId(): bool + { + return null !== $this->cid; + } + + public function getMediaType(): string + { + return $this->mediaType; + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + + if (null !== $this->cid) { + $headers->setHeaderBody('Id', 'Content-ID', $this->cid); + } + + if (null !== $this->filename) { + $headers->setHeaderParameter('Content-Disposition', 'filename', $this->filename); + } + + return $headers; + } + + public function asDebugString(): string + { + $str = parent::asDebugString(); + if (null !== $this->filename) { + $str .= ' filename: '.$this->filename; + } + + return $str; + } + + private function generateContentId(): string + { + return bin2hex(random_bytes(16)).'@symfony'; + } + + public function __destruct() + { + if (null !== $this->handle && \is_resource($this->handle)) { + fclose($this->handle); + } + } + + /** + * @return array + */ + public function __sleep() + { + // converts the body to a string + parent::__sleep(); + + $this->_parent = []; + foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) { + $r = new \ReflectionProperty(TextPart::class, $name); + $r->setAccessible(true); + $this->_parent[$name] = $r->getValue($this); + } + $this->_headers = $this->getHeaders(); + + return ['_headers', '_parent', 'filename', 'mediaType']; + } + + public function __wakeup() + { + $r = new \ReflectionProperty(AbstractPart::class, 'headers'); + $r->setAccessible(true); + $r->setValue($this, $this->_headers); + unset($this->_headers); + + if (!\is_array($this->_parent)) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) { + if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name])) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + $r = new \ReflectionProperty(TextPart::class, $name); + $r->setAccessible(true); + $r->setValue($this, $this->_parent[$name]); + } + unset($this->_parent); + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/MessagePart.php b/plugins/email/vendor/symfony/mime/Part/MessagePart.php new file mode 100644 index 0000000..1b5c23e --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/MessagePart.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @final + * + * @author Fabien Potencier + */ +class MessagePart extends DataPart +{ + private $message; + + public function __construct(RawMessage $message) + { + if ($message instanceof Message) { + $name = $message->getHeaders()->getHeaderBody('Subject').'.eml'; + } else { + $name = 'email.eml'; + } + parent::__construct('', $name); + + $this->message = $message; + } + + public function getMediaType(): string + { + return 'message'; + } + + public function getMediaSubtype(): string + { + return 'rfc822'; + } + + public function getBody(): string + { + return $this->message->toString(); + } + + public function bodyToString(): string + { + return $this->getBody(); + } + + public function bodyToIterable(): iterable + { + return $this->message->toIterable(); + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/Multipart/AlternativePart.php b/plugins/email/vendor/symfony/mime/Part/Multipart/AlternativePart.php new file mode 100644 index 0000000..fd75423 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/Multipart/AlternativePart.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; + +/** + * @author Fabien Potencier + */ +final class AlternativePart extends AbstractMultipartPart +{ + public function getMediaSubtype(): string + { + return 'alternative'; + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/Multipart/DigestPart.php b/plugins/email/vendor/symfony/mime/Part/Multipart/DigestPart.php new file mode 100644 index 0000000..27537f1 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/Multipart/DigestPart.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\MessagePart; + +/** + * @author Fabien Potencier + */ +final class DigestPart extends AbstractMultipartPart +{ + public function __construct(MessagePart ...$parts) + { + parent::__construct(...$parts); + } + + public function getMediaSubtype(): string + { + return 'digest'; + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/Multipart/FormDataPart.php b/plugins/email/vendor/symfony/mime/Part/Multipart/FormDataPart.php new file mode 100644 index 0000000..ff6df81 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/Multipart/FormDataPart.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * Implements RFC 7578. + * + * @author Fabien Potencier + */ +final class FormDataPart extends AbstractMultipartPart +{ + private $fields = []; + + /** + * @param array $fields + */ + public function __construct(array $fields = []) + { + parent::__construct(); + + foreach ($fields as $name => $value) { + if (!\is_string($value) && !\is_array($value) && !$value instanceof TextPart) { + throw new InvalidArgumentException(sprintf('A form field value can only be a string, an array, or an instance of TextPart ("%s" given).', get_debug_type($value))); + } + + $this->fields[$name] = $value; + } + // HTTP does not support \r\n in header values + $this->getHeaders()->setMaxLineLength(\PHP_INT_MAX); + } + + public function getMediaSubtype(): string + { + return 'form-data'; + } + + public function getParts(): array + { + return $this->prepareFields($this->fields); + } + + private function prepareFields(array $fields): array + { + $values = []; + + $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) { + if (\is_int($key) && \is_array($item)) { + if (1 !== \count($item)) { + throw new InvalidArgumentException(sprintf('Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, %d provided.', \count($item))); + } + + $key = key($item); + $item = $item[$key]; + } + + $fieldName = null !== $root ? sprintf('%s[%s]', $root, $key) : $key; + + if (\is_array($item)) { + array_walk($item, $prepare, $fieldName); + + return; + } + + $values[] = $this->preparePart($fieldName, $item); + }; + + array_walk($fields, $prepare); + + return $values; + } + + private function preparePart(string $name, $value): TextPart + { + if (\is_string($value)) { + return $this->configurePart($name, new TextPart($value, 'utf-8', 'plain', '8bit')); + } + + return $this->configurePart($name, $value); + } + + private function configurePart(string $name, TextPart $part): TextPart + { + static $r; + + if (null === $r) { + $r = new \ReflectionProperty(TextPart::class, 'encoding'); + $r->setAccessible(true); + } + + $part->setDisposition('form-data'); + $part->setName($name); + // HTTP does not support \r\n in header values + $part->getHeaders()->setMaxLineLength(\PHP_INT_MAX); + $r->setValue($part, '8bit'); + + return $part; + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/Multipart/MixedPart.php b/plugins/email/vendor/symfony/mime/Part/Multipart/MixedPart.php new file mode 100644 index 0000000..c8d7028 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/Multipart/MixedPart.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; + +/** + * @author Fabien Potencier + */ +final class MixedPart extends AbstractMultipartPart +{ + public function getMediaSubtype(): string + { + return 'mixed'; + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/Multipart/RelatedPart.php b/plugins/email/vendor/symfony/mime/Part/Multipart/RelatedPart.php new file mode 100644 index 0000000..08fdd5f --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/Multipart/RelatedPart.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\AbstractPart; + +/** + * @author Fabien Potencier + */ +final class RelatedPart extends AbstractMultipartPart +{ + private $mainPart; + + public function __construct(AbstractPart $mainPart, AbstractPart $part, AbstractPart ...$parts) + { + $this->mainPart = $mainPart; + $this->prepareParts($part, ...$parts); + + parent::__construct($part, ...$parts); + } + + public function getParts(): array + { + return array_merge([$this->mainPart], parent::getParts()); + } + + public function getMediaSubtype(): string + { + return 'related'; + } + + private function generateContentId(): string + { + return bin2hex(random_bytes(16)).'@symfony'; + } + + private function prepareParts(AbstractPart ...$parts): void + { + foreach ($parts as $part) { + if (!$part->getHeaders()->has('Content-ID')) { + $part->getHeaders()->setHeaderBody('Id', 'Content-ID', $this->generateContentId()); + } + } + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/SMimePart.php b/plugins/email/vendor/symfony/mime/Part/SMimePart.php new file mode 100644 index 0000000..cb619c2 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/SMimePart.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Sebastiaan Stok + */ +class SMimePart extends AbstractPart +{ + /** @internal */ + protected $_headers; + + private $body; + private $type; + private $subtype; + private $parameters; + + /** + * @param iterable|string $body + */ + public function __construct($body, string $type, string $subtype, array $parameters) + { + unset($this->_headers); + + parent::__construct(); + + if (!\is_string($body) && !is_iterable($body)) { + throw new \TypeError(sprintf('The body of "%s" must be a string or a iterable (got "%s").', self::class, get_debug_type($body))); + } + + $this->body = $body; + $this->type = $type; + $this->subtype = $subtype; + $this->parameters = $parameters; + } + + public function getMediaType(): string + { + return $this->type; + } + + public function getMediaSubtype(): string + { + return $this->subtype; + } + + public function bodyToString(): string + { + if (\is_string($this->body)) { + return $this->body; + } + + $body = ''; + foreach ($this->body as $chunk) { + $body .= $chunk; + } + $this->body = $body; + + return $body; + } + + public function bodyToIterable(): iterable + { + if (\is_string($this->body)) { + yield $this->body; + + return; + } + + $body = ''; + foreach ($this->body as $chunk) { + $body .= $chunk; + yield $chunk; + } + $this->body = $body; + } + + public function getPreparedHeaders(): Headers + { + $headers = clone parent::getHeaders(); + + $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype()); + + foreach ($this->parameters as $name => $value) { + $headers->setHeaderParameter('Content-Type', $name, $value); + } + + return $headers; + } + + public function __sleep(): array + { + // convert iterables to strings for serialization + if (is_iterable($this->body)) { + $this->body = $this->bodyToString(); + } + + $this->_headers = $this->getHeaders(); + + return ['_headers', 'body', 'type', 'subtype', 'parameters']; + } + + public function __wakeup(): void + { + $r = new \ReflectionProperty(AbstractPart::class, 'headers'); + $r->setAccessible(true); + $r->setValue($this, $this->_headers); + unset($this->_headers); + } +} diff --git a/plugins/email/vendor/symfony/mime/Part/TextPart.php b/plugins/email/vendor/symfony/mime/Part/TextPart.php new file mode 100644 index 0000000..bfe41c0 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Part/TextPart.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Encoder\Base64ContentEncoder; +use Symfony\Component\Mime\Encoder\ContentEncoderInterface; +use Symfony\Component\Mime\Encoder\EightBitContentEncoder; +use Symfony\Component\Mime\Encoder\QpContentEncoder; +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +class TextPart extends AbstractPart +{ + /** @internal */ + protected $_headers; + + private static $encoders = []; + + private $body; + private $charset; + private $subtype; + /** + * @var ?string + */ + private $disposition; + private $name; + private $encoding; + private $seekable; + + /** + * @param resource|string $body + */ + public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', string $encoding = null) + { + unset($this->_headers); + + parent::__construct(); + + if (!\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body of "%s" must be a string or a resource (got "%s").', self::class, get_debug_type($body))); + } + + $this->body = $body; + $this->charset = $charset; + $this->subtype = $subtype; + $this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null; + + if (null === $encoding) { + $this->encoding = $this->chooseEncoding(); + } else { + if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) { + throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding)); + } + $this->encoding = $encoding; + } + } + + public function getMediaType(): string + { + return 'text'; + } + + public function getMediaSubtype(): string + { + return $this->subtype; + } + + /** + * @param string $disposition one of attachment, inline, or form-data + * + * @return $this + */ + public function setDisposition(string $disposition) + { + $this->disposition = $disposition; + + return $this; + } + + /** + * Sets the name of the file (used by FormDataPart). + * + * @return $this + */ + public function setName(string $name) + { + $this->name = $name; + + return $this; + } + + public function getBody(): string + { + if (null === $this->seekable) { + return $this->body; + } + + if ($this->seekable) { + rewind($this->body); + } + + return stream_get_contents($this->body) ?: ''; + } + + public function bodyToString(): string + { + return $this->getEncoder()->encodeString($this->getBody(), $this->charset); + } + + public function bodyToIterable(): iterable + { + if (null !== $this->seekable) { + if ($this->seekable) { + rewind($this->body); + } + yield from $this->getEncoder()->encodeByteStream($this->body); + } else { + yield $this->getEncoder()->encodeString($this->body); + } + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + + $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype()); + if ($this->charset) { + $headers->setHeaderParameter('Content-Type', 'charset', $this->charset); + } + if ($this->name && 'form-data' !== $this->disposition) { + $headers->setHeaderParameter('Content-Type', 'name', $this->name); + } + $headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding); + + if (!$headers->has('Content-Disposition') && null !== $this->disposition) { + $headers->setHeaderBody('Parameterized', 'Content-Disposition', $this->disposition); + if ($this->name) { + $headers->setHeaderParameter('Content-Disposition', 'name', $this->name); + } + } + + return $headers; + } + + public function asDebugString(): string + { + $str = parent::asDebugString(); + if (null !== $this->charset) { + $str .= ' charset: '.$this->charset; + } + if (null !== $this->disposition) { + $str .= ' disposition: '.$this->disposition; + } + + return $str; + } + + private function getEncoder(): ContentEncoderInterface + { + if ('8bit' === $this->encoding) { + return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new EightBitContentEncoder()); + } + + if ('quoted-printable' === $this->encoding) { + return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new QpContentEncoder()); + } + + return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new Base64ContentEncoder()); + } + + private function chooseEncoding(): string + { + if (null === $this->charset) { + return 'base64'; + } + + return 'quoted-printable'; + } + + /** + * @return array + */ + public function __sleep() + { + // convert resources to strings for serialization + if (null !== $this->seekable) { + $this->body = $this->getBody(); + $this->seekable = null; + } + + $this->_headers = $this->getHeaders(); + + return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding']; + } + + public function __wakeup() + { + $r = new \ReflectionProperty(AbstractPart::class, 'headers'); + $r->setAccessible(true); + $r->setValue($this, $this->_headers); + unset($this->_headers); + } +} diff --git a/plugins/email/vendor/symfony/mime/README.md b/plugins/email/vendor/symfony/mime/README.md new file mode 100644 index 0000000..8e4d5c7 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/README.md @@ -0,0 +1,13 @@ +MIME Component +============== + +The MIME component allows manipulating MIME messages. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/mime.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/email/vendor/symfony/mime/RawMessage.php b/plugins/email/vendor/symfony/mime/RawMessage.php new file mode 100644 index 0000000..d2a311d --- /dev/null +++ b/plugins/email/vendor/symfony/mime/RawMessage.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; + +/** + * @author Fabien Potencier + */ +class RawMessage implements \Serializable +{ + private $message; + + /** + * @param iterable|string $message + */ + public function __construct($message) + { + $this->message = $message; + } + + public function toString(): string + { + if (\is_string($this->message)) { + return $this->message; + } + if ($this->message instanceof \Traversable) { + $this->message = iterator_to_array($this->message, false); + } + + return $this->message = implode('', $this->message); + } + + public function toIterable(): iterable + { + if (\is_string($this->message)) { + yield $this->message; + + return; + } + + $message = ''; + foreach ($this->message as $chunk) { + $message .= $chunk; + yield $chunk; + } + $this->message = $message; + } + + /** + * @throws LogicException if the message is not valid + */ + public function ensureValidity() + { + } + + /** + * @internal + */ + final public function serialize(): string + { + return serialize($this->__serialize()); + } + + /** + * @internal + */ + final public function unserialize($serialized) + { + $this->__unserialize(unserialize($serialized)); + } + + public function __serialize(): array + { + return [$this->toString()]; + } + + public function __unserialize(array $data): void + { + [$this->message] = $data; + } +} diff --git a/plugins/email/vendor/symfony/mime/Resources/bin/update_mime_types.php b/plugins/email/vendor/symfony/mime/Resources/bin/update_mime_types.php new file mode 100644 index 0000000..9f13bf3 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Resources/bin/update_mime_types.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// load new map +$data = json_decode(file_get_contents('https://cdn.jsdelivr.net/gh/jshttp/mime-db@v1.49.0/db.json'), true); +$new = []; +foreach ($data as $mimeType => $mimeTypeInformation) { + if (!array_key_exists('extensions', $mimeTypeInformation)) { + continue; + } + $new[$mimeType] = $mimeTypeInformation['extensions']; +} + +$xml = simplexml_load_string(file_get_contents('https://gitlab.freedesktop.org/xdg/shared-mime-info/-/raw/master/data/freedesktop.org.xml.in')); +foreach ($xml as $node) { + $exts = []; + foreach ($node->glob as $glob) { + $pattern = (string) $glob['pattern']; + if ('*' != $pattern[0] || '.' != $pattern[1]) { + continue; + } + + $exts[] = substr($pattern, 2); + } + + if (!$exts) { + continue; + } + + $mt = strtolower((string) $node['type']); + $new[$mt] = array_merge($new[$mt] ?? [], $exts); + foreach ($node->alias as $alias) { + $mt = strtolower((string) $alias['type']); + $new[$mt] = array_merge($new[$mt] ?? [], $exts); + } +} + +// load current map +$data = file_get_contents($output = __DIR__.'/../../MimeTypes.php'); +$current = []; +$pre = ''; +$post = ''; +foreach (explode("\n", $data) as $line) { + if (!preg_match("{^ '([^']+/[^']+)' => \['(.+)'\],$}", $line, $matches)) { + if (!$current) { + $pre .= $line."\n"; + } else { + $post .= $line."\n"; + } + continue; + } + $current[$matches[1]] = explode("', '", $matches[2]); +} + +$data = $pre; + +// reverse map +// we prefill the extensions with some preferences for content-types +$exts = [ + 'asice' => ['application/vnd.etsi.asic-e+zip'], + 'bz2' => ['application/x-bz2'], + 'csv' => ['text/csv'], + 'ecma' => ['application/ecmascript'], + 'flv' => ['video/x-flv'], + 'gif' => ['image/gif'], + 'gz' => ['application/x-gzip'], + 'htm' => ['text/html'], + 'html' => ['text/html'], + 'jar' => ['application/x-java-archive'], + 'jpg' => ['image/jpeg'], + 'js' => ['text/javascript'], + 'keynote' => ['application/vnd.apple.keynote'], + 'key' => ['application/vnd.apple.keynote'], + 'm3u' => ['audio/x-mpegurl'], + 'm4a' => ['audio/mp4'], + 'md' => ['text/markdown', 'text/x-markdown'], + 'mdb' => ['application/x-msaccess'], + 'mid' => ['audio/midi'], + 'mov' => ['video/quicktime'], + 'mp3' => ['audio/mpeg'], + 'ogg' => ['audio/ogg'], + 'pdf' => ['application/pdf'], + 'php' => ['application/x-php'], + 'ppt' => ['application/vnd.ms-powerpoint'], + 'rar' => ['application/x-rar-compressed'], + 'hqx' => ['application/stuffit'], + 'sit' => ['application/x-stuffit', 'application/stuffit'], + 'svg' => ['image/svg+xml'], + 'tar' => ['application/x-tar'], + 'tif' => ['image/tiff'], + 'ttf' => ['application/x-font-truetype'], + 'vcf' => ['text/x-vcard'], + 'wav' => ['audio/wav'], + 'wma' => ['audio/x-ms-wma'], + 'wmv' => ['audio/x-ms-wmv'], + 'xls' => ['application/vnd.ms-excel'], + 'zip' => ['application/zip'], +]; + +// we merge the 2 maps (we never remove old mime types) +$map = array_replace_recursive($current, $new); + +foreach ($exts as $ext => $types) { + foreach ($types as $mt) { + if (!isset($map[$mt])) { + $map += [$mt => [$ext]]; + } + } +} +ksort($map); + +foreach ($map as $mimeType => $extensions) { + foreach ($exts as $ext => $types) { + if (in_array($mimeType, $types, true)) { + array_unshift($extensions, $ext); + } + } + $data .= sprintf(" '%s' => ['%s'],\n", $mimeType, implode("', '", array_unique($extensions))); +} +$data .= $post; + +foreach ($map as $mimeType => $extensions) { + foreach ($extensions as $extension) { + if ('application/octet-stream' === $mimeType && 'bin' !== $extension) { + continue; + } + + $exts[$extension][] = $mimeType; + } +} +ksort($exts); + +$updated = ''; +$state = 0; +foreach (explode("\n", $data) as $line) { + if (!preg_match("{^ '([^'/]+)' => \['(.+)'\],$}", $line, $matches)) { + if (1 === $state) { + $state = 2; + foreach ($exts as $ext => $mimeTypes) { + $updated .= sprintf(" '%s' => ['%s'],\n", $ext, implode("', '", array_unique($mimeTypes))); + } + } + $updated .= $line."\n"; + continue; + } + $state = 1; +} + +$updated = preg_replace('{Updated from upstream on .+?\.}', 'Updated from upstream on '.date('Y-m-d'), $updated, -1); + +file_put_contents($output, rtrim($updated, "\n")."\n"); + +echo "Done.\n"; diff --git a/plugins/email/vendor/symfony/mime/Test/Constraint/EmailAddressContains.php b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailAddressContains.php new file mode 100644 index 0000000..827e141 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailAddressContains.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\Header\MailboxHeader; +use Symfony\Component\Mime\Header\MailboxListHeader; +use Symfony\Component\Mime\RawMessage; + +final class EmailAddressContains extends Constraint +{ + private $headerName; + private $expectedValue; + + public function __construct(string $headerName, string $expectedValue) + { + $this->headerName = $headerName; + $this->expectedValue = $expectedValue; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('contains address "%s" with value "%s"', $this->headerName, $this->expectedValue); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message)) { + throw new \LogicException('Unable to test a message address on a RawMessage instance.'); + } + + $header = $message->getHeaders()->get($this->headerName); + if ($header instanceof MailboxHeader) { + return $this->expectedValue === $header->getAddress()->getAddress(); + } elseif ($header instanceof MailboxListHeader) { + foreach ($header->getAddresses() as $address) { + if ($this->expectedValue === $address->getAddress()) { + return true; + } + } + + return false; + } + + throw new \LogicException('Unable to test a message address on a non-address header.'); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return sprintf('the Email %s (value is %s)', $this->toString(), $message->getHeaders()->get($this->headerName)->getBodyAsString()); + } +} diff --git a/plugins/email/vendor/symfony/mime/Test/Constraint/EmailAttachmentCount.php b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailAttachmentCount.php new file mode 100644 index 0000000..c0adbe3 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailAttachmentCount.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +final class EmailAttachmentCount extends Constraint +{ + private $expectedValue; + private $transport; + + public function __construct(int $expectedValue, string $transport = null) + { + $this->expectedValue = $expectedValue; + $this->transport = $transport; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has sent "%d" attachment(s)', $this->expectedValue); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message) || Message::class === \get_class($message)) { + throw new \LogicException('Unable to test a message attachment on a RawMessage or Message instance.'); + } + + return $this->expectedValue === \count($message->getAttachments()); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return 'the Email '.$this->toString(); + } +} diff --git a/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHasHeader.php b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHasHeader.php new file mode 100644 index 0000000..a29f835 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHasHeader.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\RawMessage; + +final class EmailHasHeader extends Constraint +{ + private $headerName; + + public function __construct(string $headerName) + { + $this->headerName = $headerName; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s"', $this->headerName); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message)) { + throw new \LogicException('Unable to test a message header on a RawMessage instance.'); + } + + return $message->getHeaders()->has($this->headerName); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return 'the Email '.$this->toString(); + } +} diff --git a/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHeaderSame.php b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHeaderSame.php new file mode 100644 index 0000000..74b4121 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHeaderSame.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\Header\UnstructuredHeader; +use Symfony\Component\Mime\RawMessage; + +final class EmailHeaderSame extends Constraint +{ + private $headerName; + private $expectedValue; + + public function __construct(string $headerName, string $expectedValue) + { + $this->headerName = $headerName; + $this->expectedValue = $expectedValue; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message)) { + throw new \LogicException('Unable to test a message header on a RawMessage instance.'); + } + + return $this->expectedValue === $this->getHeaderValue($message); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return sprintf('the Email %s (value is %s)', $this->toString(), $this->getHeaderValue($message) ?? 'null'); + } + + private function getHeaderValue($message): ?string + { + if (null === $header = $message->getHeaders()->get($this->headerName)) { + return null; + } + + return $header instanceof UnstructuredHeader ? $header->getValue() : $header->getBodyAsString(); + } +} diff --git a/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php new file mode 100644 index 0000000..3c61376 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +final class EmailHtmlBodyContains extends Constraint +{ + private $expectedText; + + public function __construct(string $expectedText) + { + $this->expectedText = $expectedText; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('contains "%s"', $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message) || Message::class === \get_class($message)) { + throw new \LogicException('Unable to test a message HTML body on a RawMessage or Message instance.'); + } + + return false !== mb_strpos($message->getHtmlBody(), $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function failureDescription($message): string + { + return 'the Email HTML body '.$this->toString(); + } +} diff --git a/plugins/email/vendor/symfony/mime/Test/Constraint/EmailTextBodyContains.php b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailTextBodyContains.php new file mode 100644 index 0000000..063d963 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/Test/Constraint/EmailTextBodyContains.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +final class EmailTextBodyContains extends Constraint +{ + private $expectedText; + + public function __construct(string $expectedText) + { + $this->expectedText = $expectedText; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('contains "%s"', $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message) || Message::class === \get_class($message)) { + throw new \LogicException('Unable to test a message text body on a RawMessage or Message instance.'); + } + + return false !== mb_strpos($message->getTextBody(), $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function failureDescription($message): string + { + return 'the Email text body '.$this->toString(); + } +} diff --git a/plugins/email/vendor/symfony/mime/composer.json b/plugins/email/vendor/symfony/mime/composer.json new file mode 100644 index 0000000..cd04969 --- /dev/null +++ b/plugins/email/vendor/symfony/mime/composer.json @@ -0,0 +1,46 @@ +{ + "name": "symfony/mime", + "type": "library", + "description": "Allows manipulating MIME messages", + "keywords": ["mime", "mime-type"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.1|^6.0", + "symfony/property-info": "^4.4|^5.1|^6.0", + "symfony/serializer": "^5.2|^6.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<4.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mime\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Idn.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Idn.php new file mode 100644 index 0000000..fee3026 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Idn.php @@ -0,0 +1,925 @@ + and Trevor Rowbotham + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn; + +use Exception; +use Normalizer; +use Symfony\Polyfill\Intl\Idn\Resources\unidata\DisallowedRanges; +use Symfony\Polyfill\Intl\Idn\Resources\unidata\Regex; + +/** + * @see https://www.unicode.org/reports/tr46/ + * + * @internal + */ +final class Idn +{ + public const ERROR_EMPTY_LABEL = 1; + public const ERROR_LABEL_TOO_LONG = 2; + public const ERROR_DOMAIN_NAME_TOO_LONG = 4; + public const ERROR_LEADING_HYPHEN = 8; + public const ERROR_TRAILING_HYPHEN = 0x10; + public const ERROR_HYPHEN_3_4 = 0x20; + public const ERROR_LEADING_COMBINING_MARK = 0x40; + public const ERROR_DISALLOWED = 0x80; + public const ERROR_PUNYCODE = 0x100; + public const ERROR_LABEL_HAS_DOT = 0x200; + public const ERROR_INVALID_ACE_LABEL = 0x400; + public const ERROR_BIDI = 0x800; + public const ERROR_CONTEXTJ = 0x1000; + public const ERROR_CONTEXTO_PUNCTUATION = 0x2000; + public const ERROR_CONTEXTO_DIGITS = 0x4000; + + public const INTL_IDNA_VARIANT_2003 = 0; + public const INTL_IDNA_VARIANT_UTS46 = 1; + + public const IDNA_DEFAULT = 0; + public const IDNA_ALLOW_UNASSIGNED = 1; + public const IDNA_USE_STD3_RULES = 2; + public const IDNA_CHECK_BIDI = 4; + public const IDNA_CHECK_CONTEXTJ = 8; + public const IDNA_NONTRANSITIONAL_TO_ASCII = 16; + public const IDNA_NONTRANSITIONAL_TO_UNICODE = 32; + + public const MAX_DOMAIN_SIZE = 253; + public const MAX_LABEL_SIZE = 63; + + public const BASE = 36; + public const TMIN = 1; + public const TMAX = 26; + public const SKEW = 38; + public const DAMP = 700; + public const INITIAL_BIAS = 72; + public const INITIAL_N = 128; + public const DELIMITER = '-'; + public const MAX_INT = 2147483647; + + /** + * Contains the numeric value of a basic code point (for use in representing integers) in the + * range 0 to BASE-1, or -1 if b is does not represent a value. + * + * @var array + */ + private static $basicToDigit = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, -1, + + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + /** + * @var array + */ + private static $virama; + + /** + * @var array + */ + private static $mapped; + + /** + * @var array + */ + private static $ignored; + + /** + * @var array + */ + private static $deviation; + + /** + * @var array + */ + private static $disallowed; + + /** + * @var array + */ + private static $disallowed_STD3_mapped; + + /** + * @var array + */ + private static $disallowed_STD3_valid; + + /** + * @var bool + */ + private static $mappingTableLoaded = false; + + /** + * @see https://www.unicode.org/reports/tr46/#ToASCII + * + * @param string $domainName + * @param int $options + * @param int $variant + * @param array $idna_info + * + * @return string|false + */ + public static function idn_to_ascii($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) + { + if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); + } + + $options = [ + 'CheckHyphens' => true, + 'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & self::IDNA_CHECK_BIDI), + 'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & self::IDNA_CHECK_CONTEXTJ), + 'UseSTD3ASCIIRules' => 0 !== ($options & self::IDNA_USE_STD3_RULES), + 'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & self::IDNA_NONTRANSITIONAL_TO_ASCII), + 'VerifyDnsLength' => true, + ]; + $info = new Info(); + $labels = self::process((string) $domainName, $options, $info); + + foreach ($labels as $i => $label) { + // Only convert labels to punycode that contain non-ASCII code points + if (1 === preg_match('/[^\x00-\x7F]/', $label)) { + try { + $label = 'xn--'.self::punycodeEncode($label); + } catch (Exception $e) { + $info->errors |= self::ERROR_PUNYCODE; + } + + $labels[$i] = $label; + } + } + + if ($options['VerifyDnsLength']) { + self::validateDomainAndLabelLength($labels, $info); + } + + $idna_info = [ + 'result' => implode('.', $labels), + 'isTransitionalDifferent' => $info->transitionalDifferent, + 'errors' => $info->errors, + ]; + + return 0 === $info->errors ? $idna_info['result'] : false; + } + + /** + * @see https://www.unicode.org/reports/tr46/#ToUnicode + * + * @param string $domainName + * @param int $options + * @param int $variant + * @param array $idna_info + * + * @return string|false + */ + public static function idn_to_utf8($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) + { + if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); + } + + $info = new Info(); + $labels = self::process((string) $domainName, [ + 'CheckHyphens' => true, + 'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & self::IDNA_CHECK_BIDI), + 'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & self::IDNA_CHECK_CONTEXTJ), + 'UseSTD3ASCIIRules' => 0 !== ($options & self::IDNA_USE_STD3_RULES), + 'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & self::IDNA_NONTRANSITIONAL_TO_UNICODE), + ], $info); + $idna_info = [ + 'result' => implode('.', $labels), + 'isTransitionalDifferent' => $info->transitionalDifferent, + 'errors' => $info->errors, + ]; + + return 0 === $info->errors ? $idna_info['result'] : false; + } + + /** + * @param string $label + * + * @return bool + */ + private static function isValidContextJ(array $codePoints, $label) + { + if (!isset(self::$virama)) { + self::$virama = require __DIR__.\DIRECTORY_SEPARATOR.'Resources'.\DIRECTORY_SEPARATOR.'unidata'.\DIRECTORY_SEPARATOR.'virama.php'; + } + + $offset = 0; + + foreach ($codePoints as $i => $codePoint) { + if (0x200C !== $codePoint && 0x200D !== $codePoint) { + continue; + } + + if (!isset($codePoints[$i - 1])) { + return false; + } + + // If Canonical_Combining_Class(Before(cp)) .eq. Virama Then True; + if (isset(self::$virama[$codePoints[$i - 1]])) { + continue; + } + + // If RegExpMatch((Joining_Type:{L,D})(Joining_Type:T)*\u200C(Joining_Type:T)*(Joining_Type:{R,D})) Then + // True; + // Generated RegExp = ([Joining_Type:{L,D}][Joining_Type:T]*\u200C[Joining_Type:T]*)[Joining_Type:{R,D}] + if (0x200C === $codePoint && 1 === preg_match(Regex::ZWNJ, $label, $matches, \PREG_OFFSET_CAPTURE, $offset)) { + $offset += \strlen($matches[1][0]); + + continue; + } + + return false; + } + + return true; + } + + /** + * @see https://www.unicode.org/reports/tr46/#ProcessingStepMap + * + * @param string $input + * @param array $options + * + * @return string + */ + private static function mapCodePoints($input, array $options, Info $info) + { + $str = ''; + $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules']; + $transitional = $options['Transitional_Processing']; + + foreach (self::utf8Decode($input) as $codePoint) { + $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules); + + switch ($data['status']) { + case 'disallowed': + $info->errors |= self::ERROR_DISALLOWED; + + // no break. + + case 'valid': + $str .= mb_chr($codePoint, 'utf-8'); + + break; + + case 'ignored': + // Do nothing. + break; + + case 'mapped': + $str .= $data['mapping']; + + break; + + case 'deviation': + $info->transitionalDifferent = true; + $str .= ($transitional ? $data['mapping'] : mb_chr($codePoint, 'utf-8')); + + break; + } + } + + return $str; + } + + /** + * @see https://www.unicode.org/reports/tr46/#Processing + * + * @param string $domain + * @param array $options + * + * @return array + */ + private static function process($domain, array $options, Info $info) + { + // If VerifyDnsLength is not set, we are doing ToUnicode otherwise we are doing ToASCII and + // we need to respect the VerifyDnsLength option. + $checkForEmptyLabels = !isset($options['VerifyDnsLength']) || $options['VerifyDnsLength']; + + if ($checkForEmptyLabels && '' === $domain) { + $info->errors |= self::ERROR_EMPTY_LABEL; + + return [$domain]; + } + + // Step 1. Map each code point in the domain name string + $domain = self::mapCodePoints($domain, $options, $info); + + // Step 2. Normalize the domain name string to Unicode Normalization Form C. + if (!Normalizer::isNormalized($domain, Normalizer::FORM_C)) { + $domain = Normalizer::normalize($domain, Normalizer::FORM_C); + } + + // Step 3. Break the string into labels at U+002E (.) FULL STOP. + $labels = explode('.', $domain); + $lastLabelIndex = \count($labels) - 1; + + // Step 4. Convert and validate each label in the domain name string. + foreach ($labels as $i => $label) { + $validationOptions = $options; + + if ('xn--' === substr($label, 0, 4)) { + try { + $label = self::punycodeDecode(substr($label, 4)); + } catch (Exception $e) { + $info->errors |= self::ERROR_PUNYCODE; + + continue; + } + + $validationOptions['Transitional_Processing'] = false; + $labels[$i] = $label; + } + + self::validateLabel($label, $info, $validationOptions, $i > 0 && $i === $lastLabelIndex); + } + + if ($info->bidiDomain && !$info->validBidiDomain) { + $info->errors |= self::ERROR_BIDI; + } + + // Any input domain name string that does not record an error has been successfully + // processed according to this specification. Conversely, if an input domain_name string + // causes an error, then the processing of the input domain_name string fails. Determining + // what to do with error input is up to the caller, and not in the scope of this document. + return $labels; + } + + /** + * @see https://tools.ietf.org/html/rfc5893#section-2 + * + * @param string $label + */ + private static function validateBidiLabel($label, Info $info) + { + if (1 === preg_match(Regex::RTL_LABEL, $label)) { + $info->bidiDomain = true; + + // Step 1. The first character must be a character with Bidi property L, R, or AL. + // If it has the R or AL property, it is an RTL label + if (1 !== preg_match(Regex::BIDI_STEP_1_RTL, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 2. In an RTL label, only characters with the Bidi properties R, AL, AN, EN, ES, + // CS, ET, ON, BN, or NSM are allowed. + if (1 === preg_match(Regex::BIDI_STEP_2, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 3. In an RTL label, the end of the label must be a character with Bidi property + // R, AL, EN, or AN, followed by zero or more characters with Bidi property NSM. + if (1 !== preg_match(Regex::BIDI_STEP_3, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 4. In an RTL label, if an EN is present, no AN may be present, and vice versa. + if (1 === preg_match(Regex::BIDI_STEP_4_AN, $label) && 1 === preg_match(Regex::BIDI_STEP_4_EN, $label)) { + $info->validBidiDomain = false; + + return; + } + + return; + } + + // We are a LTR label + // Step 1. The first character must be a character with Bidi property L, R, or AL. + // If it has the L property, it is an LTR label. + if (1 !== preg_match(Regex::BIDI_STEP_1_LTR, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 5. In an LTR label, only characters with the Bidi properties L, EN, + // ES, CS, ET, ON, BN, or NSM are allowed. + if (1 === preg_match(Regex::BIDI_STEP_5, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 6.In an LTR label, the end of the label must be a character with Bidi property L or + // EN, followed by zero or more characters with Bidi property NSM. + if (1 !== preg_match(Regex::BIDI_STEP_6, $label)) { + $info->validBidiDomain = false; + + return; + } + } + + /** + * @param array $labels + */ + private static function validateDomainAndLabelLength(array $labels, Info $info) + { + $maxDomainSize = self::MAX_DOMAIN_SIZE; + $length = \count($labels); + + // Number of "." delimiters. + $domainLength = $length - 1; + + // If the last label is empty and it is not the first label, then it is the root label. + // Increase the max size by 1, making it 254, to account for the root label's "." + // delimiter. This also means we don't need to check the last label's length for being too + // long. + if ($length > 1 && '' === $labels[$length - 1]) { + ++$maxDomainSize; + --$length; + } + + for ($i = 0; $i < $length; ++$i) { + $bytes = \strlen($labels[$i]); + $domainLength += $bytes; + + if ($bytes > self::MAX_LABEL_SIZE) { + $info->errors |= self::ERROR_LABEL_TOO_LONG; + } + } + + if ($domainLength > $maxDomainSize) { + $info->errors |= self::ERROR_DOMAIN_NAME_TOO_LONG; + } + } + + /** + * @see https://www.unicode.org/reports/tr46/#Validity_Criteria + * + * @param string $label + * @param array $options + * @param bool $canBeEmpty + */ + private static function validateLabel($label, Info $info, array $options, $canBeEmpty) + { + if ('' === $label) { + if (!$canBeEmpty && (!isset($options['VerifyDnsLength']) || $options['VerifyDnsLength'])) { + $info->errors |= self::ERROR_EMPTY_LABEL; + } + + return; + } + + // Step 1. The label must be in Unicode Normalization Form C. + if (!Normalizer::isNormalized($label, Normalizer::FORM_C)) { + $info->errors |= self::ERROR_INVALID_ACE_LABEL; + } + + $codePoints = self::utf8Decode($label); + + if ($options['CheckHyphens']) { + // Step 2. If CheckHyphens, the label must not contain a U+002D HYPHEN-MINUS character + // in both the thrid and fourth positions. + if (isset($codePoints[2], $codePoints[3]) && 0x002D === $codePoints[2] && 0x002D === $codePoints[3]) { + $info->errors |= self::ERROR_HYPHEN_3_4; + } + + // Step 3. If CheckHyphens, the label must neither begin nor end with a U+002D + // HYPHEN-MINUS character. + if ('-' === substr($label, 0, 1)) { + $info->errors |= self::ERROR_LEADING_HYPHEN; + } + + if ('-' === substr($label, -1, 1)) { + $info->errors |= self::ERROR_TRAILING_HYPHEN; + } + } + + // Step 4. The label must not contain a U+002E (.) FULL STOP. + if (false !== strpos($label, '.')) { + $info->errors |= self::ERROR_LABEL_HAS_DOT; + } + + // Step 5. The label must not begin with a combining mark, that is: General_Category=Mark. + if (1 === preg_match(Regex::COMBINING_MARK, $label)) { + $info->errors |= self::ERROR_LEADING_COMBINING_MARK; + } + + // Step 6. Each code point in the label must only have certain status values according to + // Section 5, IDNA Mapping Table: + $transitional = $options['Transitional_Processing']; + $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules']; + + foreach ($codePoints as $codePoint) { + $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules); + $status = $data['status']; + + if ('valid' === $status || (!$transitional && 'deviation' === $status)) { + continue; + } + + $info->errors |= self::ERROR_DISALLOWED; + + break; + } + + // Step 7. If CheckJoiners, the label must satisify the ContextJ rules from Appendix A, in + // The Unicode Code Points and Internationalized Domain Names for Applications (IDNA) + // [IDNA2008]. + if ($options['CheckJoiners'] && !self::isValidContextJ($codePoints, $label)) { + $info->errors |= self::ERROR_CONTEXTJ; + } + + // Step 8. If CheckBidi, and if the domain name is a Bidi domain name, then the label must + // satisfy all six of the numbered conditions in [IDNA2008] RFC 5893, Section 2. + if ($options['CheckBidi'] && (!$info->bidiDomain || $info->validBidiDomain)) { + self::validateBidiLabel($label, $info); + } + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.2 + * + * @param string $input + * + * @return string + */ + private static function punycodeDecode($input) + { + $n = self::INITIAL_N; + $out = 0; + $i = 0; + $bias = self::INITIAL_BIAS; + $lastDelimIndex = strrpos($input, self::DELIMITER); + $b = false === $lastDelimIndex ? 0 : $lastDelimIndex; + $inputLength = \strlen($input); + $output = []; + $bytes = array_map('ord', str_split($input)); + + for ($j = 0; $j < $b; ++$j) { + if ($bytes[$j] > 0x7F) { + throw new Exception('Invalid input'); + } + + $output[$out++] = $input[$j]; + } + + if ($b > 0) { + ++$b; + } + + for ($in = $b; $in < $inputLength; ++$out) { + $oldi = $i; + $w = 1; + + for ($k = self::BASE; /* no condition */; $k += self::BASE) { + if ($in >= $inputLength) { + throw new Exception('Invalid input'); + } + + $digit = self::$basicToDigit[$bytes[$in++] & 0xFF]; + + if ($digit < 0) { + throw new Exception('Invalid input'); + } + + if ($digit > intdiv(self::MAX_INT - $i, $w)) { + throw new Exception('Integer overflow'); + } + + $i += $digit * $w; + + if ($k <= $bias) { + $t = self::TMIN; + } elseif ($k >= $bias + self::TMAX) { + $t = self::TMAX; + } else { + $t = $k - $bias; + } + + if ($digit < $t) { + break; + } + + $baseMinusT = self::BASE - $t; + + if ($w > intdiv(self::MAX_INT, $baseMinusT)) { + throw new Exception('Integer overflow'); + } + + $w *= $baseMinusT; + } + + $outPlusOne = $out + 1; + $bias = self::adaptBias($i - $oldi, $outPlusOne, 0 === $oldi); + + if (intdiv($i, $outPlusOne) > self::MAX_INT - $n) { + throw new Exception('Integer overflow'); + } + + $n += intdiv($i, $outPlusOne); + $i %= $outPlusOne; + array_splice($output, $i++, 0, [mb_chr($n, 'utf-8')]); + } + + return implode('', $output); + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.3 + * + * @param string $input + * + * @return string + */ + private static function punycodeEncode($input) + { + $n = self::INITIAL_N; + $delta = 0; + $out = 0; + $bias = self::INITIAL_BIAS; + $inputLength = 0; + $output = ''; + $iter = self::utf8Decode($input); + + foreach ($iter as $codePoint) { + ++$inputLength; + + if ($codePoint < 0x80) { + $output .= \chr($codePoint); + ++$out; + } + } + + $h = $out; + $b = $out; + + if ($b > 0) { + $output .= self::DELIMITER; + ++$out; + } + + while ($h < $inputLength) { + $m = self::MAX_INT; + + foreach ($iter as $codePoint) { + if ($codePoint >= $n && $codePoint < $m) { + $m = $codePoint; + } + } + + if ($m - $n > intdiv(self::MAX_INT - $delta, $h + 1)) { + throw new Exception('Integer overflow'); + } + + $delta += ($m - $n) * ($h + 1); + $n = $m; + + foreach ($iter as $codePoint) { + if ($codePoint < $n && 0 === ++$delta) { + throw new Exception('Integer overflow'); + } + + if ($codePoint === $n) { + $q = $delta; + + for ($k = self::BASE; /* no condition */; $k += self::BASE) { + if ($k <= $bias) { + $t = self::TMIN; + } elseif ($k >= $bias + self::TMAX) { + $t = self::TMAX; + } else { + $t = $k - $bias; + } + + if ($q < $t) { + break; + } + + $qMinusT = $q - $t; + $baseMinusT = self::BASE - $t; + $output .= self::encodeDigit($t + ($qMinusT) % ($baseMinusT), false); + ++$out; + $q = intdiv($qMinusT, $baseMinusT); + } + + $output .= self::encodeDigit($q, false); + ++$out; + $bias = self::adaptBias($delta, $h + 1, $h === $b); + $delta = 0; + ++$h; + } + } + + ++$delta; + ++$n; + } + + return $output; + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.1 + * + * @param int $delta + * @param int $numPoints + * @param bool $firstTime + * + * @return int + */ + private static function adaptBias($delta, $numPoints, $firstTime) + { + // xxx >> 1 is a faster way of doing intdiv(xxx, 2) + $delta = $firstTime ? intdiv($delta, self::DAMP) : $delta >> 1; + $delta += intdiv($delta, $numPoints); + $k = 0; + + while ($delta > ((self::BASE - self::TMIN) * self::TMAX) >> 1) { + $delta = intdiv($delta, self::BASE - self::TMIN); + $k += self::BASE; + } + + return $k + intdiv((self::BASE - self::TMIN + 1) * $delta, $delta + self::SKEW); + } + + /** + * @param int $d + * @param bool $flag + * + * @return string + */ + private static function encodeDigit($d, $flag) + { + return \chr($d + 22 + 75 * ($d < 26 ? 1 : 0) - (($flag ? 1 : 0) << 5)); + } + + /** + * Takes a UTF-8 encoded string and converts it into a series of integer code points. Any + * invalid byte sequences will be replaced by a U+FFFD replacement code point. + * + * @see https://encoding.spec.whatwg.org/#utf-8-decoder + * + * @param string $input + * + * @return array + */ + private static function utf8Decode($input) + { + $bytesSeen = 0; + $bytesNeeded = 0; + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + $codePoint = 0; + $codePoints = []; + $length = \strlen($input); + + for ($i = 0; $i < $length; ++$i) { + $byte = \ord($input[$i]); + + if (0 === $bytesNeeded) { + if ($byte >= 0x00 && $byte <= 0x7F) { + $codePoints[] = $byte; + + continue; + } + + if ($byte >= 0xC2 && $byte <= 0xDF) { + $bytesNeeded = 1; + $codePoint = $byte & 0x1F; + } elseif ($byte >= 0xE0 && $byte <= 0xEF) { + if (0xE0 === $byte) { + $lowerBoundary = 0xA0; + } elseif (0xED === $byte) { + $upperBoundary = 0x9F; + } + + $bytesNeeded = 2; + $codePoint = $byte & 0xF; + } elseif ($byte >= 0xF0 && $byte <= 0xF4) { + if (0xF0 === $byte) { + $lowerBoundary = 0x90; + } elseif (0xF4 === $byte) { + $upperBoundary = 0x8F; + } + + $bytesNeeded = 3; + $codePoint = $byte & 0x7; + } else { + $codePoints[] = 0xFFFD; + } + + continue; + } + + if ($byte < $lowerBoundary || $byte > $upperBoundary) { + $codePoint = 0; + $bytesNeeded = 0; + $bytesSeen = 0; + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + --$i; + $codePoints[] = 0xFFFD; + + continue; + } + + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + $codePoint = ($codePoint << 6) | ($byte & 0x3F); + + if (++$bytesSeen !== $bytesNeeded) { + continue; + } + + $codePoints[] = $codePoint; + $codePoint = 0; + $bytesNeeded = 0; + $bytesSeen = 0; + } + + // String unexpectedly ended, so append a U+FFFD code point. + if (0 !== $bytesNeeded) { + $codePoints[] = 0xFFFD; + } + + return $codePoints; + } + + /** + * @param int $codePoint + * @param bool $useSTD3ASCIIRules + * + * @return array{status: string, mapping?: string} + */ + private static function lookupCodePointStatus($codePoint, $useSTD3ASCIIRules) + { + if (!self::$mappingTableLoaded) { + self::$mappingTableLoaded = true; + self::$mapped = require __DIR__.'/Resources/unidata/mapped.php'; + self::$ignored = require __DIR__.'/Resources/unidata/ignored.php'; + self::$deviation = require __DIR__.'/Resources/unidata/deviation.php'; + self::$disallowed = require __DIR__.'/Resources/unidata/disallowed.php'; + self::$disallowed_STD3_mapped = require __DIR__.'/Resources/unidata/disallowed_STD3_mapped.php'; + self::$disallowed_STD3_valid = require __DIR__.'/Resources/unidata/disallowed_STD3_valid.php'; + } + + if (isset(self::$mapped[$codePoint])) { + return ['status' => 'mapped', 'mapping' => self::$mapped[$codePoint]]; + } + + if (isset(self::$ignored[$codePoint])) { + return ['status' => 'ignored']; + } + + if (isset(self::$deviation[$codePoint])) { + return ['status' => 'deviation', 'mapping' => self::$deviation[$codePoint]]; + } + + if (isset(self::$disallowed[$codePoint]) || DisallowedRanges::inRange($codePoint)) { + return ['status' => 'disallowed']; + } + + $isDisallowedMapped = isset(self::$disallowed_STD3_mapped[$codePoint]); + + if ($isDisallowedMapped || isset(self::$disallowed_STD3_valid[$codePoint])) { + $status = 'disallowed'; + + if (!$useSTD3ASCIIRules) { + $status = $isDisallowedMapped ? 'mapped' : 'valid'; + } + + if ($isDisallowedMapped) { + return ['status' => $status, 'mapping' => self::$disallowed_STD3_mapped[$codePoint]]; + } + + return ['status' => $status]; + } + + return ['status' => 'valid']; + } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Info.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Info.php new file mode 100644 index 0000000..25c3582 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Info.php @@ -0,0 +1,23 @@ + and Trevor Rowbotham + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn; + +/** + * @internal + */ +class Info +{ + public $bidiDomain = false; + public $errors = 0; + public $validBidiDomain = true; + public $transitionalDifferent = false; +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/LICENSE b/plugins/email/vendor/symfony/polyfill-intl-idn/LICENSE new file mode 100644 index 0000000..03c5e25 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier and Trevor Rowbotham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/README.md b/plugins/email/vendor/symfony/polyfill-intl-idn/README.md new file mode 100644 index 0000000..cae5517 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/README.md @@ -0,0 +1,12 @@ +Symfony Polyfill / Intl: Idn +============================ + +This component provides [`idn_to_ascii`](https://php.net/idn-to-ascii) and [`idn_to_utf8`](https://php.net/idn-to-utf8) functions to users who run php versions without the [Intl](https://php.net/intl) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php new file mode 100644 index 0000000..5bb70e4 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php @@ -0,0 +1,375 @@ += 128 && $codePoint <= 159) { + return true; + } + + if ($codePoint >= 2155 && $codePoint <= 2207) { + return true; + } + + if ($codePoint >= 3676 && $codePoint <= 3712) { + return true; + } + + if ($codePoint >= 3808 && $codePoint <= 3839) { + return true; + } + + if ($codePoint >= 4059 && $codePoint <= 4095) { + return true; + } + + if ($codePoint >= 4256 && $codePoint <= 4293) { + return true; + } + + if ($codePoint >= 6849 && $codePoint <= 6911) { + return true; + } + + if ($codePoint >= 11859 && $codePoint <= 11903) { + return true; + } + + if ($codePoint >= 42955 && $codePoint <= 42996) { + return true; + } + + if ($codePoint >= 55296 && $codePoint <= 57343) { + return true; + } + + if ($codePoint >= 57344 && $codePoint <= 63743) { + return true; + } + + if ($codePoint >= 64218 && $codePoint <= 64255) { + return true; + } + + if ($codePoint >= 64976 && $codePoint <= 65007) { + return true; + } + + if ($codePoint >= 65630 && $codePoint <= 65663) { + return true; + } + + if ($codePoint >= 65953 && $codePoint <= 65999) { + return true; + } + + if ($codePoint >= 66046 && $codePoint <= 66175) { + return true; + } + + if ($codePoint >= 66518 && $codePoint <= 66559) { + return true; + } + + if ($codePoint >= 66928 && $codePoint <= 67071) { + return true; + } + + if ($codePoint >= 67432 && $codePoint <= 67583) { + return true; + } + + if ($codePoint >= 67760 && $codePoint <= 67807) { + return true; + } + + if ($codePoint >= 67904 && $codePoint <= 67967) { + return true; + } + + if ($codePoint >= 68256 && $codePoint <= 68287) { + return true; + } + + if ($codePoint >= 68528 && $codePoint <= 68607) { + return true; + } + + if ($codePoint >= 68681 && $codePoint <= 68735) { + return true; + } + + if ($codePoint >= 68922 && $codePoint <= 69215) { + return true; + } + + if ($codePoint >= 69298 && $codePoint <= 69375) { + return true; + } + + if ($codePoint >= 69466 && $codePoint <= 69551) { + return true; + } + + if ($codePoint >= 70207 && $codePoint <= 70271) { + return true; + } + + if ($codePoint >= 70517 && $codePoint <= 70655) { + return true; + } + + if ($codePoint >= 70874 && $codePoint <= 71039) { + return true; + } + + if ($codePoint >= 71134 && $codePoint <= 71167) { + return true; + } + + if ($codePoint >= 71370 && $codePoint <= 71423) { + return true; + } + + if ($codePoint >= 71488 && $codePoint <= 71679) { + return true; + } + + if ($codePoint >= 71740 && $codePoint <= 71839) { + return true; + } + + if ($codePoint >= 72026 && $codePoint <= 72095) { + return true; + } + + if ($codePoint >= 72441 && $codePoint <= 72703) { + return true; + } + + if ($codePoint >= 72887 && $codePoint <= 72959) { + return true; + } + + if ($codePoint >= 73130 && $codePoint <= 73439) { + return true; + } + + if ($codePoint >= 73465 && $codePoint <= 73647) { + return true; + } + + if ($codePoint >= 74650 && $codePoint <= 74751) { + return true; + } + + if ($codePoint >= 75076 && $codePoint <= 77823) { + return true; + } + + if ($codePoint >= 78905 && $codePoint <= 82943) { + return true; + } + + if ($codePoint >= 83527 && $codePoint <= 92159) { + return true; + } + + if ($codePoint >= 92784 && $codePoint <= 92879) { + return true; + } + + if ($codePoint >= 93072 && $codePoint <= 93759) { + return true; + } + + if ($codePoint >= 93851 && $codePoint <= 93951) { + return true; + } + + if ($codePoint >= 94112 && $codePoint <= 94175) { + return true; + } + + if ($codePoint >= 101590 && $codePoint <= 101631) { + return true; + } + + if ($codePoint >= 101641 && $codePoint <= 110591) { + return true; + } + + if ($codePoint >= 110879 && $codePoint <= 110927) { + return true; + } + + if ($codePoint >= 111356 && $codePoint <= 113663) { + return true; + } + + if ($codePoint >= 113828 && $codePoint <= 118783) { + return true; + } + + if ($codePoint >= 119366 && $codePoint <= 119519) { + return true; + } + + if ($codePoint >= 119673 && $codePoint <= 119807) { + return true; + } + + if ($codePoint >= 121520 && $codePoint <= 122879) { + return true; + } + + if ($codePoint >= 122923 && $codePoint <= 123135) { + return true; + } + + if ($codePoint >= 123216 && $codePoint <= 123583) { + return true; + } + + if ($codePoint >= 123648 && $codePoint <= 124927) { + return true; + } + + if ($codePoint >= 125143 && $codePoint <= 125183) { + return true; + } + + if ($codePoint >= 125280 && $codePoint <= 126064) { + return true; + } + + if ($codePoint >= 126133 && $codePoint <= 126208) { + return true; + } + + if ($codePoint >= 126270 && $codePoint <= 126463) { + return true; + } + + if ($codePoint >= 126652 && $codePoint <= 126703) { + return true; + } + + if ($codePoint >= 126706 && $codePoint <= 126975) { + return true; + } + + if ($codePoint >= 127406 && $codePoint <= 127461) { + return true; + } + + if ($codePoint >= 127590 && $codePoint <= 127743) { + return true; + } + + if ($codePoint >= 129202 && $codePoint <= 129279) { + return true; + } + + if ($codePoint >= 129751 && $codePoint <= 129791) { + return true; + } + + if ($codePoint >= 129995 && $codePoint <= 130031) { + return true; + } + + if ($codePoint >= 130042 && $codePoint <= 131069) { + return true; + } + + if ($codePoint >= 173790 && $codePoint <= 173823) { + return true; + } + + if ($codePoint >= 191457 && $codePoint <= 194559) { + return true; + } + + if ($codePoint >= 195102 && $codePoint <= 196605) { + return true; + } + + if ($codePoint >= 201547 && $codePoint <= 262141) { + return true; + } + + if ($codePoint >= 262144 && $codePoint <= 327677) { + return true; + } + + if ($codePoint >= 327680 && $codePoint <= 393213) { + return true; + } + + if ($codePoint >= 393216 && $codePoint <= 458749) { + return true; + } + + if ($codePoint >= 458752 && $codePoint <= 524285) { + return true; + } + + if ($codePoint >= 524288 && $codePoint <= 589821) { + return true; + } + + if ($codePoint >= 589824 && $codePoint <= 655357) { + return true; + } + + if ($codePoint >= 655360 && $codePoint <= 720893) { + return true; + } + + if ($codePoint >= 720896 && $codePoint <= 786429) { + return true; + } + + if ($codePoint >= 786432 && $codePoint <= 851965) { + return true; + } + + if ($codePoint >= 851968 && $codePoint <= 917501) { + return true; + } + + if ($codePoint >= 917536 && $codePoint <= 917631) { + return true; + } + + if ($codePoint >= 917632 && $codePoint <= 917759) { + return true; + } + + if ($codePoint >= 918000 && $codePoint <= 983037) { + return true; + } + + if ($codePoint >= 983040 && $codePoint <= 1048573) { + return true; + } + + if ($codePoint >= 1048576 && $codePoint <= 1114109) { + return true; + } + + return false; + } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php new file mode 100644 index 0000000..5c1c51d --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php @@ -0,0 +1,24 @@ + 'ss', + 962 => 'σ', + 8204 => '', + 8205 => '', +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php new file mode 100644 index 0000000..25a5f56 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php @@ -0,0 +1,2638 @@ + true, + 889 => true, + 896 => true, + 897 => true, + 898 => true, + 899 => true, + 907 => true, + 909 => true, + 930 => true, + 1216 => true, + 1328 => true, + 1367 => true, + 1368 => true, + 1419 => true, + 1420 => true, + 1424 => true, + 1480 => true, + 1481 => true, + 1482 => true, + 1483 => true, + 1484 => true, + 1485 => true, + 1486 => true, + 1487 => true, + 1515 => true, + 1516 => true, + 1517 => true, + 1518 => true, + 1525 => true, + 1526 => true, + 1527 => true, + 1528 => true, + 1529 => true, + 1530 => true, + 1531 => true, + 1532 => true, + 1533 => true, + 1534 => true, + 1535 => true, + 1536 => true, + 1537 => true, + 1538 => true, + 1539 => true, + 1540 => true, + 1541 => true, + 1564 => true, + 1565 => true, + 1757 => true, + 1806 => true, + 1807 => true, + 1867 => true, + 1868 => true, + 1970 => true, + 1971 => true, + 1972 => true, + 1973 => true, + 1974 => true, + 1975 => true, + 1976 => true, + 1977 => true, + 1978 => true, + 1979 => true, + 1980 => true, + 1981 => true, + 1982 => true, + 1983 => true, + 2043 => true, + 2044 => true, + 2094 => true, + 2095 => true, + 2111 => true, + 2140 => true, + 2141 => true, + 2143 => true, + 2229 => true, + 2248 => true, + 2249 => true, + 2250 => true, + 2251 => true, + 2252 => true, + 2253 => true, + 2254 => true, + 2255 => true, + 2256 => true, + 2257 => true, + 2258 => true, + 2274 => true, + 2436 => true, + 2445 => true, + 2446 => true, + 2449 => true, + 2450 => true, + 2473 => true, + 2481 => true, + 2483 => true, + 2484 => true, + 2485 => true, + 2490 => true, + 2491 => true, + 2501 => true, + 2502 => true, + 2505 => true, + 2506 => true, + 2511 => true, + 2512 => true, + 2513 => true, + 2514 => true, + 2515 => true, + 2516 => true, + 2517 => true, + 2518 => true, + 2520 => true, + 2521 => true, + 2522 => true, + 2523 => true, + 2526 => true, + 2532 => true, + 2533 => true, + 2559 => true, + 2560 => true, + 2564 => true, + 2571 => true, + 2572 => true, + 2573 => true, + 2574 => true, + 2577 => true, + 2578 => true, + 2601 => true, + 2609 => true, + 2612 => true, + 2615 => true, + 2618 => true, + 2619 => true, + 2621 => true, + 2627 => true, + 2628 => true, + 2629 => true, + 2630 => true, + 2633 => true, + 2634 => true, + 2638 => true, + 2639 => true, + 2640 => true, + 2642 => true, + 2643 => true, + 2644 => true, + 2645 => true, + 2646 => true, + 2647 => true, + 2648 => true, + 2653 => true, + 2655 => true, + 2656 => true, + 2657 => true, + 2658 => true, + 2659 => true, + 2660 => true, + 2661 => true, + 2679 => true, + 2680 => true, + 2681 => true, + 2682 => true, + 2683 => true, + 2684 => true, + 2685 => true, + 2686 => true, + 2687 => true, + 2688 => true, + 2692 => true, + 2702 => true, + 2706 => true, + 2729 => true, + 2737 => true, + 2740 => true, + 2746 => true, + 2747 => true, + 2758 => true, + 2762 => true, + 2766 => true, + 2767 => true, + 2769 => true, + 2770 => true, + 2771 => true, + 2772 => true, + 2773 => true, + 2774 => true, + 2775 => true, + 2776 => true, + 2777 => true, + 2778 => true, + 2779 => true, + 2780 => true, + 2781 => true, + 2782 => true, + 2783 => true, + 2788 => true, + 2789 => true, + 2802 => true, + 2803 => true, + 2804 => true, + 2805 => true, + 2806 => true, + 2807 => true, + 2808 => true, + 2816 => true, + 2820 => true, + 2829 => true, + 2830 => true, + 2833 => true, + 2834 => true, + 2857 => true, + 2865 => true, + 2868 => true, + 2874 => true, + 2875 => true, + 2885 => true, + 2886 => true, + 2889 => true, + 2890 => true, + 2894 => true, + 2895 => true, + 2896 => true, + 2897 => true, + 2898 => true, + 2899 => true, + 2900 => true, + 2904 => true, + 2905 => true, + 2906 => true, + 2907 => true, + 2910 => true, + 2916 => true, + 2917 => true, + 2936 => true, + 2937 => true, + 2938 => true, + 2939 => true, + 2940 => true, + 2941 => true, + 2942 => true, + 2943 => true, + 2944 => true, + 2945 => true, + 2948 => true, + 2955 => true, + 2956 => true, + 2957 => true, + 2961 => true, + 2966 => true, + 2967 => true, + 2968 => true, + 2971 => true, + 2973 => true, + 2976 => true, + 2977 => true, + 2978 => true, + 2981 => true, + 2982 => true, + 2983 => true, + 2987 => true, + 2988 => true, + 2989 => true, + 3002 => true, + 3003 => true, + 3004 => true, + 3005 => true, + 3011 => true, + 3012 => true, + 3013 => true, + 3017 => true, + 3022 => true, + 3023 => true, + 3025 => true, + 3026 => true, + 3027 => true, + 3028 => true, + 3029 => true, + 3030 => true, + 3032 => true, + 3033 => true, + 3034 => true, + 3035 => true, + 3036 => true, + 3037 => true, + 3038 => true, + 3039 => true, + 3040 => true, + 3041 => true, + 3042 => true, + 3043 => true, + 3044 => true, + 3045 => true, + 3067 => true, + 3068 => true, + 3069 => true, + 3070 => true, + 3071 => true, + 3085 => true, + 3089 => true, + 3113 => true, + 3130 => true, + 3131 => true, + 3132 => true, + 3141 => true, + 3145 => true, + 3150 => true, + 3151 => true, + 3152 => true, + 3153 => true, + 3154 => true, + 3155 => true, + 3156 => true, + 3159 => true, + 3163 => true, + 3164 => true, + 3165 => true, + 3166 => true, + 3167 => true, + 3172 => true, + 3173 => true, + 3184 => true, + 3185 => true, + 3186 => true, + 3187 => true, + 3188 => true, + 3189 => true, + 3190 => true, + 3213 => true, + 3217 => true, + 3241 => true, + 3252 => true, + 3258 => true, + 3259 => true, + 3269 => true, + 3273 => true, + 3278 => true, + 3279 => true, + 3280 => true, + 3281 => true, + 3282 => true, + 3283 => true, + 3284 => true, + 3287 => true, + 3288 => true, + 3289 => true, + 3290 => true, + 3291 => true, + 3292 => true, + 3293 => true, + 3295 => true, + 3300 => true, + 3301 => true, + 3312 => true, + 3315 => true, + 3316 => true, + 3317 => true, + 3318 => true, + 3319 => true, + 3320 => true, + 3321 => true, + 3322 => true, + 3323 => true, + 3324 => true, + 3325 => true, + 3326 => true, + 3327 => true, + 3341 => true, + 3345 => true, + 3397 => true, + 3401 => true, + 3408 => true, + 3409 => true, + 3410 => true, + 3411 => true, + 3428 => true, + 3429 => true, + 3456 => true, + 3460 => true, + 3479 => true, + 3480 => true, + 3481 => true, + 3506 => true, + 3516 => true, + 3518 => true, + 3519 => true, + 3527 => true, + 3528 => true, + 3529 => true, + 3531 => true, + 3532 => true, + 3533 => true, + 3534 => true, + 3541 => true, + 3543 => true, + 3552 => true, + 3553 => true, + 3554 => true, + 3555 => true, + 3556 => true, + 3557 => true, + 3568 => true, + 3569 => true, + 3573 => true, + 3574 => true, + 3575 => true, + 3576 => true, + 3577 => true, + 3578 => true, + 3579 => true, + 3580 => true, + 3581 => true, + 3582 => true, + 3583 => true, + 3584 => true, + 3643 => true, + 3644 => true, + 3645 => true, + 3646 => true, + 3715 => true, + 3717 => true, + 3723 => true, + 3748 => true, + 3750 => true, + 3774 => true, + 3775 => true, + 3781 => true, + 3783 => true, + 3790 => true, + 3791 => true, + 3802 => true, + 3803 => true, + 3912 => true, + 3949 => true, + 3950 => true, + 3951 => true, + 3952 => true, + 3992 => true, + 4029 => true, + 4045 => true, + 4294 => true, + 4296 => true, + 4297 => true, + 4298 => true, + 4299 => true, + 4300 => true, + 4302 => true, + 4303 => true, + 4447 => true, + 4448 => true, + 4681 => true, + 4686 => true, + 4687 => true, + 4695 => true, + 4697 => true, + 4702 => true, + 4703 => true, + 4745 => true, + 4750 => true, + 4751 => true, + 4785 => true, + 4790 => true, + 4791 => true, + 4799 => true, + 4801 => true, + 4806 => true, + 4807 => true, + 4823 => true, + 4881 => true, + 4886 => true, + 4887 => true, + 4955 => true, + 4956 => true, + 4989 => true, + 4990 => true, + 4991 => true, + 5018 => true, + 5019 => true, + 5020 => true, + 5021 => true, + 5022 => true, + 5023 => true, + 5110 => true, + 5111 => true, + 5118 => true, + 5119 => true, + 5760 => true, + 5789 => true, + 5790 => true, + 5791 => true, + 5881 => true, + 5882 => true, + 5883 => true, + 5884 => true, + 5885 => true, + 5886 => true, + 5887 => true, + 5901 => true, + 5909 => true, + 5910 => true, + 5911 => true, + 5912 => true, + 5913 => true, + 5914 => true, + 5915 => true, + 5916 => true, + 5917 => true, + 5918 => true, + 5919 => true, + 5943 => true, + 5944 => true, + 5945 => true, + 5946 => true, + 5947 => true, + 5948 => true, + 5949 => true, + 5950 => true, + 5951 => true, + 5972 => true, + 5973 => true, + 5974 => true, + 5975 => true, + 5976 => true, + 5977 => true, + 5978 => true, + 5979 => true, + 5980 => true, + 5981 => true, + 5982 => true, + 5983 => true, + 5997 => true, + 6001 => true, + 6004 => true, + 6005 => true, + 6006 => true, + 6007 => true, + 6008 => true, + 6009 => true, + 6010 => true, + 6011 => true, + 6012 => true, + 6013 => true, + 6014 => true, + 6015 => true, + 6068 => true, + 6069 => true, + 6110 => true, + 6111 => true, + 6122 => true, + 6123 => true, + 6124 => true, + 6125 => true, + 6126 => true, + 6127 => true, + 6138 => true, + 6139 => true, + 6140 => true, + 6141 => true, + 6142 => true, + 6143 => true, + 6150 => true, + 6158 => true, + 6159 => true, + 6170 => true, + 6171 => true, + 6172 => true, + 6173 => true, + 6174 => true, + 6175 => true, + 6265 => true, + 6266 => true, + 6267 => true, + 6268 => true, + 6269 => true, + 6270 => true, + 6271 => true, + 6315 => true, + 6316 => true, + 6317 => true, + 6318 => true, + 6319 => true, + 6390 => true, + 6391 => true, + 6392 => true, + 6393 => true, + 6394 => true, + 6395 => true, + 6396 => true, + 6397 => true, + 6398 => true, + 6399 => true, + 6431 => true, + 6444 => true, + 6445 => true, + 6446 => true, + 6447 => true, + 6460 => true, + 6461 => true, + 6462 => true, + 6463 => true, + 6465 => true, + 6466 => true, + 6467 => true, + 6510 => true, + 6511 => true, + 6517 => true, + 6518 => true, + 6519 => true, + 6520 => true, + 6521 => true, + 6522 => true, + 6523 => true, + 6524 => true, + 6525 => true, + 6526 => true, + 6527 => true, + 6572 => true, + 6573 => true, + 6574 => true, + 6575 => true, + 6602 => true, + 6603 => true, + 6604 => true, + 6605 => true, + 6606 => true, + 6607 => true, + 6619 => true, + 6620 => true, + 6621 => true, + 6684 => true, + 6685 => true, + 6751 => true, + 6781 => true, + 6782 => true, + 6794 => true, + 6795 => true, + 6796 => true, + 6797 => true, + 6798 => true, + 6799 => true, + 6810 => true, + 6811 => true, + 6812 => true, + 6813 => true, + 6814 => true, + 6815 => true, + 6830 => true, + 6831 => true, + 6988 => true, + 6989 => true, + 6990 => true, + 6991 => true, + 7037 => true, + 7038 => true, + 7039 => true, + 7156 => true, + 7157 => true, + 7158 => true, + 7159 => true, + 7160 => true, + 7161 => true, + 7162 => true, + 7163 => true, + 7224 => true, + 7225 => true, + 7226 => true, + 7242 => true, + 7243 => true, + 7244 => true, + 7305 => true, + 7306 => true, + 7307 => true, + 7308 => true, + 7309 => true, + 7310 => true, + 7311 => true, + 7355 => true, + 7356 => true, + 7368 => true, + 7369 => true, + 7370 => true, + 7371 => true, + 7372 => true, + 7373 => true, + 7374 => true, + 7375 => true, + 7419 => true, + 7420 => true, + 7421 => true, + 7422 => true, + 7423 => true, + 7674 => true, + 7958 => true, + 7959 => true, + 7966 => true, + 7967 => true, + 8006 => true, + 8007 => true, + 8014 => true, + 8015 => true, + 8024 => true, + 8026 => true, + 8028 => true, + 8030 => true, + 8062 => true, + 8063 => true, + 8117 => true, + 8133 => true, + 8148 => true, + 8149 => true, + 8156 => true, + 8176 => true, + 8177 => true, + 8181 => true, + 8191 => true, + 8206 => true, + 8207 => true, + 8228 => true, + 8229 => true, + 8230 => true, + 8232 => true, + 8233 => true, + 8234 => true, + 8235 => true, + 8236 => true, + 8237 => true, + 8238 => true, + 8289 => true, + 8290 => true, + 8291 => true, + 8293 => true, + 8294 => true, + 8295 => true, + 8296 => true, + 8297 => true, + 8298 => true, + 8299 => true, + 8300 => true, + 8301 => true, + 8302 => true, + 8303 => true, + 8306 => true, + 8307 => true, + 8335 => true, + 8349 => true, + 8350 => true, + 8351 => true, + 8384 => true, + 8385 => true, + 8386 => true, + 8387 => true, + 8388 => true, + 8389 => true, + 8390 => true, + 8391 => true, + 8392 => true, + 8393 => true, + 8394 => true, + 8395 => true, + 8396 => true, + 8397 => true, + 8398 => true, + 8399 => true, + 8433 => true, + 8434 => true, + 8435 => true, + 8436 => true, + 8437 => true, + 8438 => true, + 8439 => true, + 8440 => true, + 8441 => true, + 8442 => true, + 8443 => true, + 8444 => true, + 8445 => true, + 8446 => true, + 8447 => true, + 8498 => true, + 8579 => true, + 8588 => true, + 8589 => true, + 8590 => true, + 8591 => true, + 9255 => true, + 9256 => true, + 9257 => true, + 9258 => true, + 9259 => true, + 9260 => true, + 9261 => true, + 9262 => true, + 9263 => true, + 9264 => true, + 9265 => true, + 9266 => true, + 9267 => true, + 9268 => true, + 9269 => true, + 9270 => true, + 9271 => true, + 9272 => true, + 9273 => true, + 9274 => true, + 9275 => true, + 9276 => true, + 9277 => true, + 9278 => true, + 9279 => true, + 9291 => true, + 9292 => true, + 9293 => true, + 9294 => true, + 9295 => true, + 9296 => true, + 9297 => true, + 9298 => true, + 9299 => true, + 9300 => true, + 9301 => true, + 9302 => true, + 9303 => true, + 9304 => true, + 9305 => true, + 9306 => true, + 9307 => true, + 9308 => true, + 9309 => true, + 9310 => true, + 9311 => true, + 9352 => true, + 9353 => true, + 9354 => true, + 9355 => true, + 9356 => true, + 9357 => true, + 9358 => true, + 9359 => true, + 9360 => true, + 9361 => true, + 9362 => true, + 9363 => true, + 9364 => true, + 9365 => true, + 9366 => true, + 9367 => true, + 9368 => true, + 9369 => true, + 9370 => true, + 9371 => true, + 11124 => true, + 11125 => true, + 11158 => true, + 11311 => true, + 11359 => true, + 11508 => true, + 11509 => true, + 11510 => true, + 11511 => true, + 11512 => true, + 11558 => true, + 11560 => true, + 11561 => true, + 11562 => true, + 11563 => true, + 11564 => true, + 11566 => true, + 11567 => true, + 11624 => true, + 11625 => true, + 11626 => true, + 11627 => true, + 11628 => true, + 11629 => true, + 11630 => true, + 11633 => true, + 11634 => true, + 11635 => true, + 11636 => true, + 11637 => true, + 11638 => true, + 11639 => true, + 11640 => true, + 11641 => true, + 11642 => true, + 11643 => true, + 11644 => true, + 11645 => true, + 11646 => true, + 11671 => true, + 11672 => true, + 11673 => true, + 11674 => true, + 11675 => true, + 11676 => true, + 11677 => true, + 11678 => true, + 11679 => true, + 11687 => true, + 11695 => true, + 11703 => true, + 11711 => true, + 11719 => true, + 11727 => true, + 11735 => true, + 11743 => true, + 11930 => true, + 12020 => true, + 12021 => true, + 12022 => true, + 12023 => true, + 12024 => true, + 12025 => true, + 12026 => true, + 12027 => true, + 12028 => true, + 12029 => true, + 12030 => true, + 12031 => true, + 12246 => true, + 12247 => true, + 12248 => true, + 12249 => true, + 12250 => true, + 12251 => true, + 12252 => true, + 12253 => true, + 12254 => true, + 12255 => true, + 12256 => true, + 12257 => true, + 12258 => true, + 12259 => true, + 12260 => true, + 12261 => true, + 12262 => true, + 12263 => true, + 12264 => true, + 12265 => true, + 12266 => true, + 12267 => true, + 12268 => true, + 12269 => true, + 12270 => true, + 12271 => true, + 12272 => true, + 12273 => true, + 12274 => true, + 12275 => true, + 12276 => true, + 12277 => true, + 12278 => true, + 12279 => true, + 12280 => true, + 12281 => true, + 12282 => true, + 12283 => true, + 12284 => true, + 12285 => true, + 12286 => true, + 12287 => true, + 12352 => true, + 12439 => true, + 12440 => true, + 12544 => true, + 12545 => true, + 12546 => true, + 12547 => true, + 12548 => true, + 12592 => true, + 12644 => true, + 12687 => true, + 12772 => true, + 12773 => true, + 12774 => true, + 12775 => true, + 12776 => true, + 12777 => true, + 12778 => true, + 12779 => true, + 12780 => true, + 12781 => true, + 12782 => true, + 12783 => true, + 12831 => true, + 13250 => true, + 13255 => true, + 13272 => true, + 40957 => true, + 40958 => true, + 40959 => true, + 42125 => true, + 42126 => true, + 42127 => true, + 42183 => true, + 42184 => true, + 42185 => true, + 42186 => true, + 42187 => true, + 42188 => true, + 42189 => true, + 42190 => true, + 42191 => true, + 42540 => true, + 42541 => true, + 42542 => true, + 42543 => true, + 42544 => true, + 42545 => true, + 42546 => true, + 42547 => true, + 42548 => true, + 42549 => true, + 42550 => true, + 42551 => true, + 42552 => true, + 42553 => true, + 42554 => true, + 42555 => true, + 42556 => true, + 42557 => true, + 42558 => true, + 42559 => true, + 42744 => true, + 42745 => true, + 42746 => true, + 42747 => true, + 42748 => true, + 42749 => true, + 42750 => true, + 42751 => true, + 42944 => true, + 42945 => true, + 43053 => true, + 43054 => true, + 43055 => true, + 43066 => true, + 43067 => true, + 43068 => true, + 43069 => true, + 43070 => true, + 43071 => true, + 43128 => true, + 43129 => true, + 43130 => true, + 43131 => true, + 43132 => true, + 43133 => true, + 43134 => true, + 43135 => true, + 43206 => true, + 43207 => true, + 43208 => true, + 43209 => true, + 43210 => true, + 43211 => true, + 43212 => true, + 43213 => true, + 43226 => true, + 43227 => true, + 43228 => true, + 43229 => true, + 43230 => true, + 43231 => true, + 43348 => true, + 43349 => true, + 43350 => true, + 43351 => true, + 43352 => true, + 43353 => true, + 43354 => true, + 43355 => true, + 43356 => true, + 43357 => true, + 43358 => true, + 43389 => true, + 43390 => true, + 43391 => true, + 43470 => true, + 43482 => true, + 43483 => true, + 43484 => true, + 43485 => true, + 43519 => true, + 43575 => true, + 43576 => true, + 43577 => true, + 43578 => true, + 43579 => true, + 43580 => true, + 43581 => true, + 43582 => true, + 43583 => true, + 43598 => true, + 43599 => true, + 43610 => true, + 43611 => true, + 43715 => true, + 43716 => true, + 43717 => true, + 43718 => true, + 43719 => true, + 43720 => true, + 43721 => true, + 43722 => true, + 43723 => true, + 43724 => true, + 43725 => true, + 43726 => true, + 43727 => true, + 43728 => true, + 43729 => true, + 43730 => true, + 43731 => true, + 43732 => true, + 43733 => true, + 43734 => true, + 43735 => true, + 43736 => true, + 43737 => true, + 43738 => true, + 43767 => true, + 43768 => true, + 43769 => true, + 43770 => true, + 43771 => true, + 43772 => true, + 43773 => true, + 43774 => true, + 43775 => true, + 43776 => true, + 43783 => true, + 43784 => true, + 43791 => true, + 43792 => true, + 43799 => true, + 43800 => true, + 43801 => true, + 43802 => true, + 43803 => true, + 43804 => true, + 43805 => true, + 43806 => true, + 43807 => true, + 43815 => true, + 43823 => true, + 43884 => true, + 43885 => true, + 43886 => true, + 43887 => true, + 44014 => true, + 44015 => true, + 44026 => true, + 44027 => true, + 44028 => true, + 44029 => true, + 44030 => true, + 44031 => true, + 55204 => true, + 55205 => true, + 55206 => true, + 55207 => true, + 55208 => true, + 55209 => true, + 55210 => true, + 55211 => true, + 55212 => true, + 55213 => true, + 55214 => true, + 55215 => true, + 55239 => true, + 55240 => true, + 55241 => true, + 55242 => true, + 55292 => true, + 55293 => true, + 55294 => true, + 55295 => true, + 64110 => true, + 64111 => true, + 64263 => true, + 64264 => true, + 64265 => true, + 64266 => true, + 64267 => true, + 64268 => true, + 64269 => true, + 64270 => true, + 64271 => true, + 64272 => true, + 64273 => true, + 64274 => true, + 64280 => true, + 64281 => true, + 64282 => true, + 64283 => true, + 64284 => true, + 64311 => true, + 64317 => true, + 64319 => true, + 64322 => true, + 64325 => true, + 64450 => true, + 64451 => true, + 64452 => true, + 64453 => true, + 64454 => true, + 64455 => true, + 64456 => true, + 64457 => true, + 64458 => true, + 64459 => true, + 64460 => true, + 64461 => true, + 64462 => true, + 64463 => true, + 64464 => true, + 64465 => true, + 64466 => true, + 64832 => true, + 64833 => true, + 64834 => true, + 64835 => true, + 64836 => true, + 64837 => true, + 64838 => true, + 64839 => true, + 64840 => true, + 64841 => true, + 64842 => true, + 64843 => true, + 64844 => true, + 64845 => true, + 64846 => true, + 64847 => true, + 64912 => true, + 64913 => true, + 64968 => true, + 64969 => true, + 64970 => true, + 64971 => true, + 64972 => true, + 64973 => true, + 64974 => true, + 64975 => true, + 65022 => true, + 65023 => true, + 65042 => true, + 65049 => true, + 65050 => true, + 65051 => true, + 65052 => true, + 65053 => true, + 65054 => true, + 65055 => true, + 65072 => true, + 65106 => true, + 65107 => true, + 65127 => true, + 65132 => true, + 65133 => true, + 65134 => true, + 65135 => true, + 65141 => true, + 65277 => true, + 65278 => true, + 65280 => true, + 65440 => true, + 65471 => true, + 65472 => true, + 65473 => true, + 65480 => true, + 65481 => true, + 65488 => true, + 65489 => true, + 65496 => true, + 65497 => true, + 65501 => true, + 65502 => true, + 65503 => true, + 65511 => true, + 65519 => true, + 65520 => true, + 65521 => true, + 65522 => true, + 65523 => true, + 65524 => true, + 65525 => true, + 65526 => true, + 65527 => true, + 65528 => true, + 65529 => true, + 65530 => true, + 65531 => true, + 65532 => true, + 65533 => true, + 65534 => true, + 65535 => true, + 65548 => true, + 65575 => true, + 65595 => true, + 65598 => true, + 65614 => true, + 65615 => true, + 65787 => true, + 65788 => true, + 65789 => true, + 65790 => true, + 65791 => true, + 65795 => true, + 65796 => true, + 65797 => true, + 65798 => true, + 65844 => true, + 65845 => true, + 65846 => true, + 65935 => true, + 65949 => true, + 65950 => true, + 65951 => true, + 66205 => true, + 66206 => true, + 66207 => true, + 66257 => true, + 66258 => true, + 66259 => true, + 66260 => true, + 66261 => true, + 66262 => true, + 66263 => true, + 66264 => true, + 66265 => true, + 66266 => true, + 66267 => true, + 66268 => true, + 66269 => true, + 66270 => true, + 66271 => true, + 66300 => true, + 66301 => true, + 66302 => true, + 66303 => true, + 66340 => true, + 66341 => true, + 66342 => true, + 66343 => true, + 66344 => true, + 66345 => true, + 66346 => true, + 66347 => true, + 66348 => true, + 66379 => true, + 66380 => true, + 66381 => true, + 66382 => true, + 66383 => true, + 66427 => true, + 66428 => true, + 66429 => true, + 66430 => true, + 66431 => true, + 66462 => true, + 66500 => true, + 66501 => true, + 66502 => true, + 66503 => true, + 66718 => true, + 66719 => true, + 66730 => true, + 66731 => true, + 66732 => true, + 66733 => true, + 66734 => true, + 66735 => true, + 66772 => true, + 66773 => true, + 66774 => true, + 66775 => true, + 66812 => true, + 66813 => true, + 66814 => true, + 66815 => true, + 66856 => true, + 66857 => true, + 66858 => true, + 66859 => true, + 66860 => true, + 66861 => true, + 66862 => true, + 66863 => true, + 66916 => true, + 66917 => true, + 66918 => true, + 66919 => true, + 66920 => true, + 66921 => true, + 66922 => true, + 66923 => true, + 66924 => true, + 66925 => true, + 66926 => true, + 67383 => true, + 67384 => true, + 67385 => true, + 67386 => true, + 67387 => true, + 67388 => true, + 67389 => true, + 67390 => true, + 67391 => true, + 67414 => true, + 67415 => true, + 67416 => true, + 67417 => true, + 67418 => true, + 67419 => true, + 67420 => true, + 67421 => true, + 67422 => true, + 67423 => true, + 67590 => true, + 67591 => true, + 67593 => true, + 67638 => true, + 67641 => true, + 67642 => true, + 67643 => true, + 67645 => true, + 67646 => true, + 67670 => true, + 67743 => true, + 67744 => true, + 67745 => true, + 67746 => true, + 67747 => true, + 67748 => true, + 67749 => true, + 67750 => true, + 67827 => true, + 67830 => true, + 67831 => true, + 67832 => true, + 67833 => true, + 67834 => true, + 67868 => true, + 67869 => true, + 67870 => true, + 67898 => true, + 67899 => true, + 67900 => true, + 67901 => true, + 67902 => true, + 68024 => true, + 68025 => true, + 68026 => true, + 68027 => true, + 68048 => true, + 68049 => true, + 68100 => true, + 68103 => true, + 68104 => true, + 68105 => true, + 68106 => true, + 68107 => true, + 68116 => true, + 68120 => true, + 68150 => true, + 68151 => true, + 68155 => true, + 68156 => true, + 68157 => true, + 68158 => true, + 68169 => true, + 68170 => true, + 68171 => true, + 68172 => true, + 68173 => true, + 68174 => true, + 68175 => true, + 68185 => true, + 68186 => true, + 68187 => true, + 68188 => true, + 68189 => true, + 68190 => true, + 68191 => true, + 68327 => true, + 68328 => true, + 68329 => true, + 68330 => true, + 68343 => true, + 68344 => true, + 68345 => true, + 68346 => true, + 68347 => true, + 68348 => true, + 68349 => true, + 68350 => true, + 68351 => true, + 68406 => true, + 68407 => true, + 68408 => true, + 68438 => true, + 68439 => true, + 68467 => true, + 68468 => true, + 68469 => true, + 68470 => true, + 68471 => true, + 68498 => true, + 68499 => true, + 68500 => true, + 68501 => true, + 68502 => true, + 68503 => true, + 68504 => true, + 68509 => true, + 68510 => true, + 68511 => true, + 68512 => true, + 68513 => true, + 68514 => true, + 68515 => true, + 68516 => true, + 68517 => true, + 68518 => true, + 68519 => true, + 68520 => true, + 68787 => true, + 68788 => true, + 68789 => true, + 68790 => true, + 68791 => true, + 68792 => true, + 68793 => true, + 68794 => true, + 68795 => true, + 68796 => true, + 68797 => true, + 68798 => true, + 68799 => true, + 68851 => true, + 68852 => true, + 68853 => true, + 68854 => true, + 68855 => true, + 68856 => true, + 68857 => true, + 68904 => true, + 68905 => true, + 68906 => true, + 68907 => true, + 68908 => true, + 68909 => true, + 68910 => true, + 68911 => true, + 69247 => true, + 69290 => true, + 69294 => true, + 69295 => true, + 69416 => true, + 69417 => true, + 69418 => true, + 69419 => true, + 69420 => true, + 69421 => true, + 69422 => true, + 69423 => true, + 69580 => true, + 69581 => true, + 69582 => true, + 69583 => true, + 69584 => true, + 69585 => true, + 69586 => true, + 69587 => true, + 69588 => true, + 69589 => true, + 69590 => true, + 69591 => true, + 69592 => true, + 69593 => true, + 69594 => true, + 69595 => true, + 69596 => true, + 69597 => true, + 69598 => true, + 69599 => true, + 69623 => true, + 69624 => true, + 69625 => true, + 69626 => true, + 69627 => true, + 69628 => true, + 69629 => true, + 69630 => true, + 69631 => true, + 69710 => true, + 69711 => true, + 69712 => true, + 69713 => true, + 69744 => true, + 69745 => true, + 69746 => true, + 69747 => true, + 69748 => true, + 69749 => true, + 69750 => true, + 69751 => true, + 69752 => true, + 69753 => true, + 69754 => true, + 69755 => true, + 69756 => true, + 69757 => true, + 69758 => true, + 69821 => true, + 69826 => true, + 69827 => true, + 69828 => true, + 69829 => true, + 69830 => true, + 69831 => true, + 69832 => true, + 69833 => true, + 69834 => true, + 69835 => true, + 69836 => true, + 69837 => true, + 69838 => true, + 69839 => true, + 69865 => true, + 69866 => true, + 69867 => true, + 69868 => true, + 69869 => true, + 69870 => true, + 69871 => true, + 69882 => true, + 69883 => true, + 69884 => true, + 69885 => true, + 69886 => true, + 69887 => true, + 69941 => true, + 69960 => true, + 69961 => true, + 69962 => true, + 69963 => true, + 69964 => true, + 69965 => true, + 69966 => true, + 69967 => true, + 70007 => true, + 70008 => true, + 70009 => true, + 70010 => true, + 70011 => true, + 70012 => true, + 70013 => true, + 70014 => true, + 70015 => true, + 70112 => true, + 70133 => true, + 70134 => true, + 70135 => true, + 70136 => true, + 70137 => true, + 70138 => true, + 70139 => true, + 70140 => true, + 70141 => true, + 70142 => true, + 70143 => true, + 70162 => true, + 70279 => true, + 70281 => true, + 70286 => true, + 70302 => true, + 70314 => true, + 70315 => true, + 70316 => true, + 70317 => true, + 70318 => true, + 70319 => true, + 70379 => true, + 70380 => true, + 70381 => true, + 70382 => true, + 70383 => true, + 70394 => true, + 70395 => true, + 70396 => true, + 70397 => true, + 70398 => true, + 70399 => true, + 70404 => true, + 70413 => true, + 70414 => true, + 70417 => true, + 70418 => true, + 70441 => true, + 70449 => true, + 70452 => true, + 70458 => true, + 70469 => true, + 70470 => true, + 70473 => true, + 70474 => true, + 70478 => true, + 70479 => true, + 70481 => true, + 70482 => true, + 70483 => true, + 70484 => true, + 70485 => true, + 70486 => true, + 70488 => true, + 70489 => true, + 70490 => true, + 70491 => true, + 70492 => true, + 70500 => true, + 70501 => true, + 70509 => true, + 70510 => true, + 70511 => true, + 70748 => true, + 70754 => true, + 70755 => true, + 70756 => true, + 70757 => true, + 70758 => true, + 70759 => true, + 70760 => true, + 70761 => true, + 70762 => true, + 70763 => true, + 70764 => true, + 70765 => true, + 70766 => true, + 70767 => true, + 70768 => true, + 70769 => true, + 70770 => true, + 70771 => true, + 70772 => true, + 70773 => true, + 70774 => true, + 70775 => true, + 70776 => true, + 70777 => true, + 70778 => true, + 70779 => true, + 70780 => true, + 70781 => true, + 70782 => true, + 70783 => true, + 70856 => true, + 70857 => true, + 70858 => true, + 70859 => true, + 70860 => true, + 70861 => true, + 70862 => true, + 70863 => true, + 71094 => true, + 71095 => true, + 71237 => true, + 71238 => true, + 71239 => true, + 71240 => true, + 71241 => true, + 71242 => true, + 71243 => true, + 71244 => true, + 71245 => true, + 71246 => true, + 71247 => true, + 71258 => true, + 71259 => true, + 71260 => true, + 71261 => true, + 71262 => true, + 71263 => true, + 71277 => true, + 71278 => true, + 71279 => true, + 71280 => true, + 71281 => true, + 71282 => true, + 71283 => true, + 71284 => true, + 71285 => true, + 71286 => true, + 71287 => true, + 71288 => true, + 71289 => true, + 71290 => true, + 71291 => true, + 71292 => true, + 71293 => true, + 71294 => true, + 71295 => true, + 71353 => true, + 71354 => true, + 71355 => true, + 71356 => true, + 71357 => true, + 71358 => true, + 71359 => true, + 71451 => true, + 71452 => true, + 71468 => true, + 71469 => true, + 71470 => true, + 71471 => true, + 71923 => true, + 71924 => true, + 71925 => true, + 71926 => true, + 71927 => true, + 71928 => true, + 71929 => true, + 71930 => true, + 71931 => true, + 71932 => true, + 71933 => true, + 71934 => true, + 71943 => true, + 71944 => true, + 71946 => true, + 71947 => true, + 71956 => true, + 71959 => true, + 71990 => true, + 71993 => true, + 71994 => true, + 72007 => true, + 72008 => true, + 72009 => true, + 72010 => true, + 72011 => true, + 72012 => true, + 72013 => true, + 72014 => true, + 72015 => true, + 72104 => true, + 72105 => true, + 72152 => true, + 72153 => true, + 72165 => true, + 72166 => true, + 72167 => true, + 72168 => true, + 72169 => true, + 72170 => true, + 72171 => true, + 72172 => true, + 72173 => true, + 72174 => true, + 72175 => true, + 72176 => true, + 72177 => true, + 72178 => true, + 72179 => true, + 72180 => true, + 72181 => true, + 72182 => true, + 72183 => true, + 72184 => true, + 72185 => true, + 72186 => true, + 72187 => true, + 72188 => true, + 72189 => true, + 72190 => true, + 72191 => true, + 72264 => true, + 72265 => true, + 72266 => true, + 72267 => true, + 72268 => true, + 72269 => true, + 72270 => true, + 72271 => true, + 72355 => true, + 72356 => true, + 72357 => true, + 72358 => true, + 72359 => true, + 72360 => true, + 72361 => true, + 72362 => true, + 72363 => true, + 72364 => true, + 72365 => true, + 72366 => true, + 72367 => true, + 72368 => true, + 72369 => true, + 72370 => true, + 72371 => true, + 72372 => true, + 72373 => true, + 72374 => true, + 72375 => true, + 72376 => true, + 72377 => true, + 72378 => true, + 72379 => true, + 72380 => true, + 72381 => true, + 72382 => true, + 72383 => true, + 72713 => true, + 72759 => true, + 72774 => true, + 72775 => true, + 72776 => true, + 72777 => true, + 72778 => true, + 72779 => true, + 72780 => true, + 72781 => true, + 72782 => true, + 72783 => true, + 72813 => true, + 72814 => true, + 72815 => true, + 72848 => true, + 72849 => true, + 72872 => true, + 72967 => true, + 72970 => true, + 73015 => true, + 73016 => true, + 73017 => true, + 73019 => true, + 73022 => true, + 73032 => true, + 73033 => true, + 73034 => true, + 73035 => true, + 73036 => true, + 73037 => true, + 73038 => true, + 73039 => true, + 73050 => true, + 73051 => true, + 73052 => true, + 73053 => true, + 73054 => true, + 73055 => true, + 73062 => true, + 73065 => true, + 73103 => true, + 73106 => true, + 73113 => true, + 73114 => true, + 73115 => true, + 73116 => true, + 73117 => true, + 73118 => true, + 73119 => true, + 73649 => true, + 73650 => true, + 73651 => true, + 73652 => true, + 73653 => true, + 73654 => true, + 73655 => true, + 73656 => true, + 73657 => true, + 73658 => true, + 73659 => true, + 73660 => true, + 73661 => true, + 73662 => true, + 73663 => true, + 73714 => true, + 73715 => true, + 73716 => true, + 73717 => true, + 73718 => true, + 73719 => true, + 73720 => true, + 73721 => true, + 73722 => true, + 73723 => true, + 73724 => true, + 73725 => true, + 73726 => true, + 74863 => true, + 74869 => true, + 74870 => true, + 74871 => true, + 74872 => true, + 74873 => true, + 74874 => true, + 74875 => true, + 74876 => true, + 74877 => true, + 74878 => true, + 74879 => true, + 78895 => true, + 78896 => true, + 78897 => true, + 78898 => true, + 78899 => true, + 78900 => true, + 78901 => true, + 78902 => true, + 78903 => true, + 78904 => true, + 92729 => true, + 92730 => true, + 92731 => true, + 92732 => true, + 92733 => true, + 92734 => true, + 92735 => true, + 92767 => true, + 92778 => true, + 92779 => true, + 92780 => true, + 92781 => true, + 92910 => true, + 92911 => true, + 92918 => true, + 92919 => true, + 92920 => true, + 92921 => true, + 92922 => true, + 92923 => true, + 92924 => true, + 92925 => true, + 92926 => true, + 92927 => true, + 92998 => true, + 92999 => true, + 93000 => true, + 93001 => true, + 93002 => true, + 93003 => true, + 93004 => true, + 93005 => true, + 93006 => true, + 93007 => true, + 93018 => true, + 93026 => true, + 93048 => true, + 93049 => true, + 93050 => true, + 93051 => true, + 93052 => true, + 94027 => true, + 94028 => true, + 94029 => true, + 94030 => true, + 94088 => true, + 94089 => true, + 94090 => true, + 94091 => true, + 94092 => true, + 94093 => true, + 94094 => true, + 94181 => true, + 94182 => true, + 94183 => true, + 94184 => true, + 94185 => true, + 94186 => true, + 94187 => true, + 94188 => true, + 94189 => true, + 94190 => true, + 94191 => true, + 94194 => true, + 94195 => true, + 94196 => true, + 94197 => true, + 94198 => true, + 94199 => true, + 94200 => true, + 94201 => true, + 94202 => true, + 94203 => true, + 94204 => true, + 94205 => true, + 94206 => true, + 94207 => true, + 100344 => true, + 100345 => true, + 100346 => true, + 100347 => true, + 100348 => true, + 100349 => true, + 100350 => true, + 100351 => true, + 110931 => true, + 110932 => true, + 110933 => true, + 110934 => true, + 110935 => true, + 110936 => true, + 110937 => true, + 110938 => true, + 110939 => true, + 110940 => true, + 110941 => true, + 110942 => true, + 110943 => true, + 110944 => true, + 110945 => true, + 110946 => true, + 110947 => true, + 110952 => true, + 110953 => true, + 110954 => true, + 110955 => true, + 110956 => true, + 110957 => true, + 110958 => true, + 110959 => true, + 113771 => true, + 113772 => true, + 113773 => true, + 113774 => true, + 113775 => true, + 113789 => true, + 113790 => true, + 113791 => true, + 113801 => true, + 113802 => true, + 113803 => true, + 113804 => true, + 113805 => true, + 113806 => true, + 113807 => true, + 113818 => true, + 113819 => true, + 119030 => true, + 119031 => true, + 119032 => true, + 119033 => true, + 119034 => true, + 119035 => true, + 119036 => true, + 119037 => true, + 119038 => true, + 119039 => true, + 119079 => true, + 119080 => true, + 119155 => true, + 119156 => true, + 119157 => true, + 119158 => true, + 119159 => true, + 119160 => true, + 119161 => true, + 119162 => true, + 119273 => true, + 119274 => true, + 119275 => true, + 119276 => true, + 119277 => true, + 119278 => true, + 119279 => true, + 119280 => true, + 119281 => true, + 119282 => true, + 119283 => true, + 119284 => true, + 119285 => true, + 119286 => true, + 119287 => true, + 119288 => true, + 119289 => true, + 119290 => true, + 119291 => true, + 119292 => true, + 119293 => true, + 119294 => true, + 119295 => true, + 119540 => true, + 119541 => true, + 119542 => true, + 119543 => true, + 119544 => true, + 119545 => true, + 119546 => true, + 119547 => true, + 119548 => true, + 119549 => true, + 119550 => true, + 119551 => true, + 119639 => true, + 119640 => true, + 119641 => true, + 119642 => true, + 119643 => true, + 119644 => true, + 119645 => true, + 119646 => true, + 119647 => true, + 119893 => true, + 119965 => true, + 119968 => true, + 119969 => true, + 119971 => true, + 119972 => true, + 119975 => true, + 119976 => true, + 119981 => true, + 119994 => true, + 119996 => true, + 120004 => true, + 120070 => true, + 120075 => true, + 120076 => true, + 120085 => true, + 120093 => true, + 120122 => true, + 120127 => true, + 120133 => true, + 120135 => true, + 120136 => true, + 120137 => true, + 120145 => true, + 120486 => true, + 120487 => true, + 120780 => true, + 120781 => true, + 121484 => true, + 121485 => true, + 121486 => true, + 121487 => true, + 121488 => true, + 121489 => true, + 121490 => true, + 121491 => true, + 121492 => true, + 121493 => true, + 121494 => true, + 121495 => true, + 121496 => true, + 121497 => true, + 121498 => true, + 121504 => true, + 122887 => true, + 122905 => true, + 122906 => true, + 122914 => true, + 122917 => true, + 123181 => true, + 123182 => true, + 123183 => true, + 123198 => true, + 123199 => true, + 123210 => true, + 123211 => true, + 123212 => true, + 123213 => true, + 123642 => true, + 123643 => true, + 123644 => true, + 123645 => true, + 123646 => true, + 125125 => true, + 125126 => true, + 125260 => true, + 125261 => true, + 125262 => true, + 125263 => true, + 125274 => true, + 125275 => true, + 125276 => true, + 125277 => true, + 126468 => true, + 126496 => true, + 126499 => true, + 126501 => true, + 126502 => true, + 126504 => true, + 126515 => true, + 126520 => true, + 126522 => true, + 126524 => true, + 126525 => true, + 126526 => true, + 126527 => true, + 126528 => true, + 126529 => true, + 126531 => true, + 126532 => true, + 126533 => true, + 126534 => true, + 126536 => true, + 126538 => true, + 126540 => true, + 126544 => true, + 126547 => true, + 126549 => true, + 126550 => true, + 126552 => true, + 126554 => true, + 126556 => true, + 126558 => true, + 126560 => true, + 126563 => true, + 126565 => true, + 126566 => true, + 126571 => true, + 126579 => true, + 126584 => true, + 126589 => true, + 126591 => true, + 126602 => true, + 126620 => true, + 126621 => true, + 126622 => true, + 126623 => true, + 126624 => true, + 126628 => true, + 126634 => true, + 127020 => true, + 127021 => true, + 127022 => true, + 127023 => true, + 127124 => true, + 127125 => true, + 127126 => true, + 127127 => true, + 127128 => true, + 127129 => true, + 127130 => true, + 127131 => true, + 127132 => true, + 127133 => true, + 127134 => true, + 127135 => true, + 127151 => true, + 127152 => true, + 127168 => true, + 127184 => true, + 127222 => true, + 127223 => true, + 127224 => true, + 127225 => true, + 127226 => true, + 127227 => true, + 127228 => true, + 127229 => true, + 127230 => true, + 127231 => true, + 127232 => true, + 127491 => true, + 127492 => true, + 127493 => true, + 127494 => true, + 127495 => true, + 127496 => true, + 127497 => true, + 127498 => true, + 127499 => true, + 127500 => true, + 127501 => true, + 127502 => true, + 127503 => true, + 127548 => true, + 127549 => true, + 127550 => true, + 127551 => true, + 127561 => true, + 127562 => true, + 127563 => true, + 127564 => true, + 127565 => true, + 127566 => true, + 127567 => true, + 127570 => true, + 127571 => true, + 127572 => true, + 127573 => true, + 127574 => true, + 127575 => true, + 127576 => true, + 127577 => true, + 127578 => true, + 127579 => true, + 127580 => true, + 127581 => true, + 127582 => true, + 127583 => true, + 128728 => true, + 128729 => true, + 128730 => true, + 128731 => true, + 128732 => true, + 128733 => true, + 128734 => true, + 128735 => true, + 128749 => true, + 128750 => true, + 128751 => true, + 128765 => true, + 128766 => true, + 128767 => true, + 128884 => true, + 128885 => true, + 128886 => true, + 128887 => true, + 128888 => true, + 128889 => true, + 128890 => true, + 128891 => true, + 128892 => true, + 128893 => true, + 128894 => true, + 128895 => true, + 128985 => true, + 128986 => true, + 128987 => true, + 128988 => true, + 128989 => true, + 128990 => true, + 128991 => true, + 129004 => true, + 129005 => true, + 129006 => true, + 129007 => true, + 129008 => true, + 129009 => true, + 129010 => true, + 129011 => true, + 129012 => true, + 129013 => true, + 129014 => true, + 129015 => true, + 129016 => true, + 129017 => true, + 129018 => true, + 129019 => true, + 129020 => true, + 129021 => true, + 129022 => true, + 129023 => true, + 129036 => true, + 129037 => true, + 129038 => true, + 129039 => true, + 129096 => true, + 129097 => true, + 129098 => true, + 129099 => true, + 129100 => true, + 129101 => true, + 129102 => true, + 129103 => true, + 129114 => true, + 129115 => true, + 129116 => true, + 129117 => true, + 129118 => true, + 129119 => true, + 129160 => true, + 129161 => true, + 129162 => true, + 129163 => true, + 129164 => true, + 129165 => true, + 129166 => true, + 129167 => true, + 129198 => true, + 129199 => true, + 129401 => true, + 129484 => true, + 129620 => true, + 129621 => true, + 129622 => true, + 129623 => true, + 129624 => true, + 129625 => true, + 129626 => true, + 129627 => true, + 129628 => true, + 129629 => true, + 129630 => true, + 129631 => true, + 129646 => true, + 129647 => true, + 129653 => true, + 129654 => true, + 129655 => true, + 129659 => true, + 129660 => true, + 129661 => true, + 129662 => true, + 129663 => true, + 129671 => true, + 129672 => true, + 129673 => true, + 129674 => true, + 129675 => true, + 129676 => true, + 129677 => true, + 129678 => true, + 129679 => true, + 129705 => true, + 129706 => true, + 129707 => true, + 129708 => true, + 129709 => true, + 129710 => true, + 129711 => true, + 129719 => true, + 129720 => true, + 129721 => true, + 129722 => true, + 129723 => true, + 129724 => true, + 129725 => true, + 129726 => true, + 129727 => true, + 129731 => true, + 129732 => true, + 129733 => true, + 129734 => true, + 129735 => true, + 129736 => true, + 129737 => true, + 129738 => true, + 129739 => true, + 129740 => true, + 129741 => true, + 129742 => true, + 129743 => true, + 129939 => true, + 131070 => true, + 131071 => true, + 177973 => true, + 177974 => true, + 177975 => true, + 177976 => true, + 177977 => true, + 177978 => true, + 177979 => true, + 177980 => true, + 177981 => true, + 177982 => true, + 177983 => true, + 178206 => true, + 178207 => true, + 183970 => true, + 183971 => true, + 183972 => true, + 183973 => true, + 183974 => true, + 183975 => true, + 183976 => true, + 183977 => true, + 183978 => true, + 183979 => true, + 183980 => true, + 183981 => true, + 183982 => true, + 183983 => true, + 194664 => true, + 194676 => true, + 194847 => true, + 194911 => true, + 195007 => true, + 196606 => true, + 196607 => true, + 262142 => true, + 262143 => true, + 327678 => true, + 327679 => true, + 393214 => true, + 393215 => true, + 458750 => true, + 458751 => true, + 524286 => true, + 524287 => true, + 589822 => true, + 589823 => true, + 655358 => true, + 655359 => true, + 720894 => true, + 720895 => true, + 786430 => true, + 786431 => true, + 851966 => true, + 851967 => true, + 917502 => true, + 917503 => true, + 917504 => true, + 917505 => true, + 917506 => true, + 917507 => true, + 917508 => true, + 917509 => true, + 917510 => true, + 917511 => true, + 917512 => true, + 917513 => true, + 917514 => true, + 917515 => true, + 917516 => true, + 917517 => true, + 917518 => true, + 917519 => true, + 917520 => true, + 917521 => true, + 917522 => true, + 917523 => true, + 917524 => true, + 917525 => true, + 917526 => true, + 917527 => true, + 917528 => true, + 917529 => true, + 917530 => true, + 917531 => true, + 917532 => true, + 917533 => true, + 917534 => true, + 917535 => true, + 983038 => true, + 983039 => true, + 1048574 => true, + 1048575 => true, + 1114110 => true, + 1114111 => true, +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php new file mode 100644 index 0000000..54f21cc --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php @@ -0,0 +1,308 @@ + ' ', + 168 => ' ̈', + 175 => ' ̄', + 180 => ' ́', + 184 => ' ̧', + 728 => ' ̆', + 729 => ' ̇', + 730 => ' ̊', + 731 => ' ̨', + 732 => ' ̃', + 733 => ' ̋', + 890 => ' ι', + 894 => ';', + 900 => ' ́', + 901 => ' ̈́', + 8125 => ' ̓', + 8127 => ' ̓', + 8128 => ' ͂', + 8129 => ' ̈͂', + 8141 => ' ̓̀', + 8142 => ' ̓́', + 8143 => ' ̓͂', + 8157 => ' ̔̀', + 8158 => ' ̔́', + 8159 => ' ̔͂', + 8173 => ' ̈̀', + 8174 => ' ̈́', + 8175 => '`', + 8189 => ' ́', + 8190 => ' ̔', + 8192 => ' ', + 8193 => ' ', + 8194 => ' ', + 8195 => ' ', + 8196 => ' ', + 8197 => ' ', + 8198 => ' ', + 8199 => ' ', + 8200 => ' ', + 8201 => ' ', + 8202 => ' ', + 8215 => ' ̳', + 8239 => ' ', + 8252 => '!!', + 8254 => ' ̅', + 8263 => '??', + 8264 => '?!', + 8265 => '!?', + 8287 => ' ', + 8314 => '+', + 8316 => '=', + 8317 => '(', + 8318 => ')', + 8330 => '+', + 8332 => '=', + 8333 => '(', + 8334 => ')', + 8448 => 'a/c', + 8449 => 'a/s', + 8453 => 'c/o', + 8454 => 'c/u', + 9332 => '(1)', + 9333 => '(2)', + 9334 => '(3)', + 9335 => '(4)', + 9336 => '(5)', + 9337 => '(6)', + 9338 => '(7)', + 9339 => '(8)', + 9340 => '(9)', + 9341 => '(10)', + 9342 => '(11)', + 9343 => '(12)', + 9344 => '(13)', + 9345 => '(14)', + 9346 => '(15)', + 9347 => '(16)', + 9348 => '(17)', + 9349 => '(18)', + 9350 => '(19)', + 9351 => '(20)', + 9372 => '(a)', + 9373 => '(b)', + 9374 => '(c)', + 9375 => '(d)', + 9376 => '(e)', + 9377 => '(f)', + 9378 => '(g)', + 9379 => '(h)', + 9380 => '(i)', + 9381 => '(j)', + 9382 => '(k)', + 9383 => '(l)', + 9384 => '(m)', + 9385 => '(n)', + 9386 => '(o)', + 9387 => '(p)', + 9388 => '(q)', + 9389 => '(r)', + 9390 => '(s)', + 9391 => '(t)', + 9392 => '(u)', + 9393 => '(v)', + 9394 => '(w)', + 9395 => '(x)', + 9396 => '(y)', + 9397 => '(z)', + 10868 => '::=', + 10869 => '==', + 10870 => '===', + 12288 => ' ', + 12443 => ' ゙', + 12444 => ' ゚', + 12800 => '(ᄀ)', + 12801 => '(ᄂ)', + 12802 => '(ᄃ)', + 12803 => '(ᄅ)', + 12804 => '(ᄆ)', + 12805 => '(ᄇ)', + 12806 => '(ᄉ)', + 12807 => '(ᄋ)', + 12808 => '(ᄌ)', + 12809 => '(ᄎ)', + 12810 => '(ᄏ)', + 12811 => '(ᄐ)', + 12812 => '(ᄑ)', + 12813 => '(ᄒ)', + 12814 => '(가)', + 12815 => '(나)', + 12816 => '(다)', + 12817 => '(라)', + 12818 => '(마)', + 12819 => '(바)', + 12820 => '(사)', + 12821 => '(아)', + 12822 => '(자)', + 12823 => '(차)', + 12824 => '(카)', + 12825 => '(타)', + 12826 => '(파)', + 12827 => '(하)', + 12828 => '(주)', + 12829 => '(오전)', + 12830 => '(오후)', + 12832 => '(一)', + 12833 => '(二)', + 12834 => '(三)', + 12835 => '(四)', + 12836 => '(五)', + 12837 => '(六)', + 12838 => '(七)', + 12839 => '(八)', + 12840 => '(九)', + 12841 => '(十)', + 12842 => '(月)', + 12843 => '(火)', + 12844 => '(水)', + 12845 => '(木)', + 12846 => '(金)', + 12847 => '(土)', + 12848 => '(日)', + 12849 => '(株)', + 12850 => '(有)', + 12851 => '(社)', + 12852 => '(名)', + 12853 => '(特)', + 12854 => '(財)', + 12855 => '(祝)', + 12856 => '(労)', + 12857 => '(代)', + 12858 => '(呼)', + 12859 => '(学)', + 12860 => '(監)', + 12861 => '(企)', + 12862 => '(資)', + 12863 => '(協)', + 12864 => '(祭)', + 12865 => '(休)', + 12866 => '(自)', + 12867 => '(至)', + 64297 => '+', + 64606 => ' ٌّ', + 64607 => ' ٍّ', + 64608 => ' َّ', + 64609 => ' ُّ', + 64610 => ' ِّ', + 64611 => ' ّٰ', + 65018 => 'صلى الله عليه وسلم', + 65019 => 'جل جلاله', + 65040 => ',', + 65043 => ':', + 65044 => ';', + 65045 => '!', + 65046 => '?', + 65075 => '_', + 65076 => '_', + 65077 => '(', + 65078 => ')', + 65079 => '{', + 65080 => '}', + 65095 => '[', + 65096 => ']', + 65097 => ' ̅', + 65098 => ' ̅', + 65099 => ' ̅', + 65100 => ' ̅', + 65101 => '_', + 65102 => '_', + 65103 => '_', + 65104 => ',', + 65108 => ';', + 65109 => ':', + 65110 => '?', + 65111 => '!', + 65113 => '(', + 65114 => ')', + 65115 => '{', + 65116 => '}', + 65119 => '#', + 65120 => '&', + 65121 => '*', + 65122 => '+', + 65124 => '<', + 65125 => '>', + 65126 => '=', + 65128 => '\\', + 65129 => '$', + 65130 => '%', + 65131 => '@', + 65136 => ' ً', + 65138 => ' ٌ', + 65140 => ' ٍ', + 65142 => ' َ', + 65144 => ' ُ', + 65146 => ' ِ', + 65148 => ' ّ', + 65150 => ' ْ', + 65281 => '!', + 65282 => '"', + 65283 => '#', + 65284 => '$', + 65285 => '%', + 65286 => '&', + 65287 => '\'', + 65288 => '(', + 65289 => ')', + 65290 => '*', + 65291 => '+', + 65292 => ',', + 65295 => '/', + 65306 => ':', + 65307 => ';', + 65308 => '<', + 65309 => '=', + 65310 => '>', + 65311 => '?', + 65312 => '@', + 65339 => '[', + 65340 => '\\', + 65341 => ']', + 65342 => '^', + 65343 => '_', + 65344 => '`', + 65371 => '{', + 65372 => '|', + 65373 => '}', + 65374 => '~', + 65507 => ' ̄', + 127233 => '0,', + 127234 => '1,', + 127235 => '2,', + 127236 => '3,', + 127237 => '4,', + 127238 => '5,', + 127239 => '6,', + 127240 => '7,', + 127241 => '8,', + 127242 => '9,', + 127248 => '(a)', + 127249 => '(b)', + 127250 => '(c)', + 127251 => '(d)', + 127252 => '(e)', + 127253 => '(f)', + 127254 => '(g)', + 127255 => '(h)', + 127256 => '(i)', + 127257 => '(j)', + 127258 => '(k)', + 127259 => '(l)', + 127260 => '(m)', + 127261 => '(n)', + 127262 => '(o)', + 127263 => '(p)', + 127264 => '(q)', + 127265 => '(r)', + 127266 => '(s)', + 127267 => '(t)', + 127268 => '(u)', + 127269 => '(v)', + 127270 => '(w)', + 127271 => '(x)', + 127272 => '(y)', + 127273 => '(z)', +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php new file mode 100644 index 0000000..223396e --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php @@ -0,0 +1,71 @@ + true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 9 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + 24 => true, + 25 => true, + 26 => true, + 27 => true, + 28 => true, + 29 => true, + 30 => true, + 31 => true, + 32 => true, + 33 => true, + 34 => true, + 35 => true, + 36 => true, + 37 => true, + 38 => true, + 39 => true, + 40 => true, + 41 => true, + 42 => true, + 43 => true, + 44 => true, + 47 => true, + 58 => true, + 59 => true, + 60 => true, + 61 => true, + 62 => true, + 63 => true, + 64 => true, + 91 => true, + 92 => true, + 93 => true, + 94 => true, + 95 => true, + 96 => true, + 123 => true, + 124 => true, + 125 => true, + 126 => true, + 127 => true, + 8800 => true, + 8814 => true, + 8815 => true, +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php new file mode 100644 index 0000000..b377844 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php @@ -0,0 +1,273 @@ + true, + 847 => true, + 6155 => true, + 6156 => true, + 6157 => true, + 8203 => true, + 8288 => true, + 8292 => true, + 65024 => true, + 65025 => true, + 65026 => true, + 65027 => true, + 65028 => true, + 65029 => true, + 65030 => true, + 65031 => true, + 65032 => true, + 65033 => true, + 65034 => true, + 65035 => true, + 65036 => true, + 65037 => true, + 65038 => true, + 65039 => true, + 65279 => true, + 113824 => true, + 113825 => true, + 113826 => true, + 113827 => true, + 917760 => true, + 917761 => true, + 917762 => true, + 917763 => true, + 917764 => true, + 917765 => true, + 917766 => true, + 917767 => true, + 917768 => true, + 917769 => true, + 917770 => true, + 917771 => true, + 917772 => true, + 917773 => true, + 917774 => true, + 917775 => true, + 917776 => true, + 917777 => true, + 917778 => true, + 917779 => true, + 917780 => true, + 917781 => true, + 917782 => true, + 917783 => true, + 917784 => true, + 917785 => true, + 917786 => true, + 917787 => true, + 917788 => true, + 917789 => true, + 917790 => true, + 917791 => true, + 917792 => true, + 917793 => true, + 917794 => true, + 917795 => true, + 917796 => true, + 917797 => true, + 917798 => true, + 917799 => true, + 917800 => true, + 917801 => true, + 917802 => true, + 917803 => true, + 917804 => true, + 917805 => true, + 917806 => true, + 917807 => true, + 917808 => true, + 917809 => true, + 917810 => true, + 917811 => true, + 917812 => true, + 917813 => true, + 917814 => true, + 917815 => true, + 917816 => true, + 917817 => true, + 917818 => true, + 917819 => true, + 917820 => true, + 917821 => true, + 917822 => true, + 917823 => true, + 917824 => true, + 917825 => true, + 917826 => true, + 917827 => true, + 917828 => true, + 917829 => true, + 917830 => true, + 917831 => true, + 917832 => true, + 917833 => true, + 917834 => true, + 917835 => true, + 917836 => true, + 917837 => true, + 917838 => true, + 917839 => true, + 917840 => true, + 917841 => true, + 917842 => true, + 917843 => true, + 917844 => true, + 917845 => true, + 917846 => true, + 917847 => true, + 917848 => true, + 917849 => true, + 917850 => true, + 917851 => true, + 917852 => true, + 917853 => true, + 917854 => true, + 917855 => true, + 917856 => true, + 917857 => true, + 917858 => true, + 917859 => true, + 917860 => true, + 917861 => true, + 917862 => true, + 917863 => true, + 917864 => true, + 917865 => true, + 917866 => true, + 917867 => true, + 917868 => true, + 917869 => true, + 917870 => true, + 917871 => true, + 917872 => true, + 917873 => true, + 917874 => true, + 917875 => true, + 917876 => true, + 917877 => true, + 917878 => true, + 917879 => true, + 917880 => true, + 917881 => true, + 917882 => true, + 917883 => true, + 917884 => true, + 917885 => true, + 917886 => true, + 917887 => true, + 917888 => true, + 917889 => true, + 917890 => true, + 917891 => true, + 917892 => true, + 917893 => true, + 917894 => true, + 917895 => true, + 917896 => true, + 917897 => true, + 917898 => true, + 917899 => true, + 917900 => true, + 917901 => true, + 917902 => true, + 917903 => true, + 917904 => true, + 917905 => true, + 917906 => true, + 917907 => true, + 917908 => true, + 917909 => true, + 917910 => true, + 917911 => true, + 917912 => true, + 917913 => true, + 917914 => true, + 917915 => true, + 917916 => true, + 917917 => true, + 917918 => true, + 917919 => true, + 917920 => true, + 917921 => true, + 917922 => true, + 917923 => true, + 917924 => true, + 917925 => true, + 917926 => true, + 917927 => true, + 917928 => true, + 917929 => true, + 917930 => true, + 917931 => true, + 917932 => true, + 917933 => true, + 917934 => true, + 917935 => true, + 917936 => true, + 917937 => true, + 917938 => true, + 917939 => true, + 917940 => true, + 917941 => true, + 917942 => true, + 917943 => true, + 917944 => true, + 917945 => true, + 917946 => true, + 917947 => true, + 917948 => true, + 917949 => true, + 917950 => true, + 917951 => true, + 917952 => true, + 917953 => true, + 917954 => true, + 917955 => true, + 917956 => true, + 917957 => true, + 917958 => true, + 917959 => true, + 917960 => true, + 917961 => true, + 917962 => true, + 917963 => true, + 917964 => true, + 917965 => true, + 917966 => true, + 917967 => true, + 917968 => true, + 917969 => true, + 917970 => true, + 917971 => true, + 917972 => true, + 917973 => true, + 917974 => true, + 917975 => true, + 917976 => true, + 917977 => true, + 917978 => true, + 917979 => true, + 917980 => true, + 917981 => true, + 917982 => true, + 917983 => true, + 917984 => true, + 917985 => true, + 917986 => true, + 917987 => true, + 917988 => true, + 917989 => true, + 917990 => true, + 917991 => true, + 917992 => true, + 917993 => true, + 917994 => true, + 917995 => true, + 917996 => true, + 917997 => true, + 917998 => true, + 917999 => true, +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php new file mode 100644 index 0000000..9b85fe9 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php @@ -0,0 +1,5778 @@ + 'a', + 66 => 'b', + 67 => 'c', + 68 => 'd', + 69 => 'e', + 70 => 'f', + 71 => 'g', + 72 => 'h', + 73 => 'i', + 74 => 'j', + 75 => 'k', + 76 => 'l', + 77 => 'm', + 78 => 'n', + 79 => 'o', + 80 => 'p', + 81 => 'q', + 82 => 'r', + 83 => 's', + 84 => 't', + 85 => 'u', + 86 => 'v', + 87 => 'w', + 88 => 'x', + 89 => 'y', + 90 => 'z', + 170 => 'a', + 178 => '2', + 179 => '3', + 181 => 'μ', + 185 => '1', + 186 => 'o', + 188 => '1⁄4', + 189 => '1⁄2', + 190 => '3⁄4', + 192 => 'à', + 193 => 'á', + 194 => 'â', + 195 => 'ã', + 196 => 'ä', + 197 => 'å', + 198 => 'æ', + 199 => 'ç', + 200 => 'è', + 201 => 'é', + 202 => 'ê', + 203 => 'ë', + 204 => 'ì', + 205 => 'í', + 206 => 'î', + 207 => 'ï', + 208 => 'ð', + 209 => 'ñ', + 210 => 'ò', + 211 => 'ó', + 212 => 'ô', + 213 => 'õ', + 214 => 'ö', + 216 => 'ø', + 217 => 'ù', + 218 => 'ú', + 219 => 'û', + 220 => 'ü', + 221 => 'ý', + 222 => 'þ', + 256 => 'ā', + 258 => 'ă', + 260 => 'ą', + 262 => 'ć', + 264 => 'ĉ', + 266 => 'ċ', + 268 => 'č', + 270 => 'ď', + 272 => 'đ', + 274 => 'ē', + 276 => 'ĕ', + 278 => 'ė', + 280 => 'ę', + 282 => 'ě', + 284 => 'ĝ', + 286 => 'ğ', + 288 => 'ġ', + 290 => 'ģ', + 292 => 'ĥ', + 294 => 'ħ', + 296 => 'ĩ', + 298 => 'ī', + 300 => 'ĭ', + 302 => 'į', + 304 => 'i̇', + 306 => 'ij', + 307 => 'ij', + 308 => 'ĵ', + 310 => 'ķ', + 313 => 'ĺ', + 315 => 'ļ', + 317 => 'ľ', + 319 => 'l·', + 320 => 'l·', + 321 => 'ł', + 323 => 'ń', + 325 => 'ņ', + 327 => 'ň', + 329 => 'ʼn', + 330 => 'ŋ', + 332 => 'ō', + 334 => 'ŏ', + 336 => 'ő', + 338 => 'œ', + 340 => 'ŕ', + 342 => 'ŗ', + 344 => 'ř', + 346 => 'ś', + 348 => 'ŝ', + 350 => 'ş', + 352 => 'š', + 354 => 'ţ', + 356 => 'ť', + 358 => 'ŧ', + 360 => 'ũ', + 362 => 'ū', + 364 => 'ŭ', + 366 => 'ů', + 368 => 'ű', + 370 => 'ų', + 372 => 'ŵ', + 374 => 'ŷ', + 376 => 'ÿ', + 377 => 'ź', + 379 => 'ż', + 381 => 'ž', + 383 => 's', + 385 => 'ɓ', + 386 => 'ƃ', + 388 => 'ƅ', + 390 => 'ɔ', + 391 => 'ƈ', + 393 => 'ɖ', + 394 => 'ɗ', + 395 => 'ƌ', + 398 => 'ǝ', + 399 => 'ə', + 400 => 'ɛ', + 401 => 'ƒ', + 403 => 'ɠ', + 404 => 'ɣ', + 406 => 'ɩ', + 407 => 'ɨ', + 408 => 'ƙ', + 412 => 'ɯ', + 413 => 'ɲ', + 415 => 'ɵ', + 416 => 'ơ', + 418 => 'ƣ', + 420 => 'ƥ', + 422 => 'ʀ', + 423 => 'ƨ', + 425 => 'ʃ', + 428 => 'ƭ', + 430 => 'ʈ', + 431 => 'ư', + 433 => 'ʊ', + 434 => 'ʋ', + 435 => 'ƴ', + 437 => 'ƶ', + 439 => 'ʒ', + 440 => 'ƹ', + 444 => 'ƽ', + 452 => 'dž', + 453 => 'dž', + 454 => 'dž', + 455 => 'lj', + 456 => 'lj', + 457 => 'lj', + 458 => 'nj', + 459 => 'nj', + 460 => 'nj', + 461 => 'ǎ', + 463 => 'ǐ', + 465 => 'ǒ', + 467 => 'ǔ', + 469 => 'ǖ', + 471 => 'ǘ', + 473 => 'ǚ', + 475 => 'ǜ', + 478 => 'ǟ', + 480 => 'ǡ', + 482 => 'ǣ', + 484 => 'ǥ', + 486 => 'ǧ', + 488 => 'ǩ', + 490 => 'ǫ', + 492 => 'ǭ', + 494 => 'ǯ', + 497 => 'dz', + 498 => 'dz', + 499 => 'dz', + 500 => 'ǵ', + 502 => 'ƕ', + 503 => 'ƿ', + 504 => 'ǹ', + 506 => 'ǻ', + 508 => 'ǽ', + 510 => 'ǿ', + 512 => 'ȁ', + 514 => 'ȃ', + 516 => 'ȅ', + 518 => 'ȇ', + 520 => 'ȉ', + 522 => 'ȋ', + 524 => 'ȍ', + 526 => 'ȏ', + 528 => 'ȑ', + 530 => 'ȓ', + 532 => 'ȕ', + 534 => 'ȗ', + 536 => 'ș', + 538 => 'ț', + 540 => 'ȝ', + 542 => 'ȟ', + 544 => 'ƞ', + 546 => 'ȣ', + 548 => 'ȥ', + 550 => 'ȧ', + 552 => 'ȩ', + 554 => 'ȫ', + 556 => 'ȭ', + 558 => 'ȯ', + 560 => 'ȱ', + 562 => 'ȳ', + 570 => 'ⱥ', + 571 => 'ȼ', + 573 => 'ƚ', + 574 => 'ⱦ', + 577 => 'ɂ', + 579 => 'ƀ', + 580 => 'ʉ', + 581 => 'ʌ', + 582 => 'ɇ', + 584 => 'ɉ', + 586 => 'ɋ', + 588 => 'ɍ', + 590 => 'ɏ', + 688 => 'h', + 689 => 'ɦ', + 690 => 'j', + 691 => 'r', + 692 => 'ɹ', + 693 => 'ɻ', + 694 => 'ʁ', + 695 => 'w', + 696 => 'y', + 736 => 'ɣ', + 737 => 'l', + 738 => 's', + 739 => 'x', + 740 => 'ʕ', + 832 => '̀', + 833 => '́', + 835 => '̓', + 836 => '̈́', + 837 => 'ι', + 880 => 'ͱ', + 882 => 'ͳ', + 884 => 'ʹ', + 886 => 'ͷ', + 895 => 'ϳ', + 902 => 'ά', + 903 => '·', + 904 => 'έ', + 905 => 'ή', + 906 => 'ί', + 908 => 'ό', + 910 => 'ύ', + 911 => 'ώ', + 913 => 'α', + 914 => 'β', + 915 => 'γ', + 916 => 'δ', + 917 => 'ε', + 918 => 'ζ', + 919 => 'η', + 920 => 'θ', + 921 => 'ι', + 922 => 'κ', + 923 => 'λ', + 924 => 'μ', + 925 => 'ν', + 926 => 'ξ', + 927 => 'ο', + 928 => 'π', + 929 => 'ρ', + 931 => 'σ', + 932 => 'τ', + 933 => 'υ', + 934 => 'φ', + 935 => 'χ', + 936 => 'ψ', + 937 => 'ω', + 938 => 'ϊ', + 939 => 'ϋ', + 975 => 'ϗ', + 976 => 'β', + 977 => 'θ', + 978 => 'υ', + 979 => 'ύ', + 980 => 'ϋ', + 981 => 'φ', + 982 => 'π', + 984 => 'ϙ', + 986 => 'ϛ', + 988 => 'ϝ', + 990 => 'ϟ', + 992 => 'ϡ', + 994 => 'ϣ', + 996 => 'ϥ', + 998 => 'ϧ', + 1000 => 'ϩ', + 1002 => 'ϫ', + 1004 => 'ϭ', + 1006 => 'ϯ', + 1008 => 'κ', + 1009 => 'ρ', + 1010 => 'σ', + 1012 => 'θ', + 1013 => 'ε', + 1015 => 'ϸ', + 1017 => 'σ', + 1018 => 'ϻ', + 1021 => 'ͻ', + 1022 => 'ͼ', + 1023 => 'ͽ', + 1024 => 'ѐ', + 1025 => 'ё', + 1026 => 'ђ', + 1027 => 'ѓ', + 1028 => 'є', + 1029 => 'ѕ', + 1030 => 'і', + 1031 => 'ї', + 1032 => 'ј', + 1033 => 'љ', + 1034 => 'њ', + 1035 => 'ћ', + 1036 => 'ќ', + 1037 => 'ѝ', + 1038 => 'ў', + 1039 => 'џ', + 1040 => 'а', + 1041 => 'б', + 1042 => 'в', + 1043 => 'г', + 1044 => 'д', + 1045 => 'е', + 1046 => 'ж', + 1047 => 'з', + 1048 => 'и', + 1049 => 'й', + 1050 => 'к', + 1051 => 'л', + 1052 => 'м', + 1053 => 'н', + 1054 => 'о', + 1055 => 'п', + 1056 => 'р', + 1057 => 'с', + 1058 => 'т', + 1059 => 'у', + 1060 => 'ф', + 1061 => 'х', + 1062 => 'ц', + 1063 => 'ч', + 1064 => 'ш', + 1065 => 'щ', + 1066 => 'ъ', + 1067 => 'ы', + 1068 => 'ь', + 1069 => 'э', + 1070 => 'ю', + 1071 => 'я', + 1120 => 'ѡ', + 1122 => 'ѣ', + 1124 => 'ѥ', + 1126 => 'ѧ', + 1128 => 'ѩ', + 1130 => 'ѫ', + 1132 => 'ѭ', + 1134 => 'ѯ', + 1136 => 'ѱ', + 1138 => 'ѳ', + 1140 => 'ѵ', + 1142 => 'ѷ', + 1144 => 'ѹ', + 1146 => 'ѻ', + 1148 => 'ѽ', + 1150 => 'ѿ', + 1152 => 'ҁ', + 1162 => 'ҋ', + 1164 => 'ҍ', + 1166 => 'ҏ', + 1168 => 'ґ', + 1170 => 'ғ', + 1172 => 'ҕ', + 1174 => 'җ', + 1176 => 'ҙ', + 1178 => 'қ', + 1180 => 'ҝ', + 1182 => 'ҟ', + 1184 => 'ҡ', + 1186 => 'ң', + 1188 => 'ҥ', + 1190 => 'ҧ', + 1192 => 'ҩ', + 1194 => 'ҫ', + 1196 => 'ҭ', + 1198 => 'ү', + 1200 => 'ұ', + 1202 => 'ҳ', + 1204 => 'ҵ', + 1206 => 'ҷ', + 1208 => 'ҹ', + 1210 => 'һ', + 1212 => 'ҽ', + 1214 => 'ҿ', + 1217 => 'ӂ', + 1219 => 'ӄ', + 1221 => 'ӆ', + 1223 => 'ӈ', + 1225 => 'ӊ', + 1227 => 'ӌ', + 1229 => 'ӎ', + 1232 => 'ӑ', + 1234 => 'ӓ', + 1236 => 'ӕ', + 1238 => 'ӗ', + 1240 => 'ә', + 1242 => 'ӛ', + 1244 => 'ӝ', + 1246 => 'ӟ', + 1248 => 'ӡ', + 1250 => 'ӣ', + 1252 => 'ӥ', + 1254 => 'ӧ', + 1256 => 'ө', + 1258 => 'ӫ', + 1260 => 'ӭ', + 1262 => 'ӯ', + 1264 => 'ӱ', + 1266 => 'ӳ', + 1268 => 'ӵ', + 1270 => 'ӷ', + 1272 => 'ӹ', + 1274 => 'ӻ', + 1276 => 'ӽ', + 1278 => 'ӿ', + 1280 => 'ԁ', + 1282 => 'ԃ', + 1284 => 'ԅ', + 1286 => 'ԇ', + 1288 => 'ԉ', + 1290 => 'ԋ', + 1292 => 'ԍ', + 1294 => 'ԏ', + 1296 => 'ԑ', + 1298 => 'ԓ', + 1300 => 'ԕ', + 1302 => 'ԗ', + 1304 => 'ԙ', + 1306 => 'ԛ', + 1308 => 'ԝ', + 1310 => 'ԟ', + 1312 => 'ԡ', + 1314 => 'ԣ', + 1316 => 'ԥ', + 1318 => 'ԧ', + 1320 => 'ԩ', + 1322 => 'ԫ', + 1324 => 'ԭ', + 1326 => 'ԯ', + 1329 => 'ա', + 1330 => 'բ', + 1331 => 'գ', + 1332 => 'դ', + 1333 => 'ե', + 1334 => 'զ', + 1335 => 'է', + 1336 => 'ը', + 1337 => 'թ', + 1338 => 'ժ', + 1339 => 'ի', + 1340 => 'լ', + 1341 => 'խ', + 1342 => 'ծ', + 1343 => 'կ', + 1344 => 'հ', + 1345 => 'ձ', + 1346 => 'ղ', + 1347 => 'ճ', + 1348 => 'մ', + 1349 => 'յ', + 1350 => 'ն', + 1351 => 'շ', + 1352 => 'ո', + 1353 => 'չ', + 1354 => 'պ', + 1355 => 'ջ', + 1356 => 'ռ', + 1357 => 'ս', + 1358 => 'վ', + 1359 => 'տ', + 1360 => 'ր', + 1361 => 'ց', + 1362 => 'ւ', + 1363 => 'փ', + 1364 => 'ք', + 1365 => 'օ', + 1366 => 'ֆ', + 1415 => 'եւ', + 1653 => 'اٴ', + 1654 => 'وٴ', + 1655 => 'ۇٴ', + 1656 => 'يٴ', + 2392 => 'क़', + 2393 => 'ख़', + 2394 => 'ग़', + 2395 => 'ज़', + 2396 => 'ड़', + 2397 => 'ढ़', + 2398 => 'फ़', + 2399 => 'य़', + 2524 => 'ড়', + 2525 => 'ঢ়', + 2527 => 'য়', + 2611 => 'ਲ਼', + 2614 => 'ਸ਼', + 2649 => 'ਖ਼', + 2650 => 'ਗ਼', + 2651 => 'ਜ਼', + 2654 => 'ਫ਼', + 2908 => 'ଡ଼', + 2909 => 'ଢ଼', + 3635 => 'ํา', + 3763 => 'ໍາ', + 3804 => 'ຫນ', + 3805 => 'ຫມ', + 3852 => '་', + 3907 => 'གྷ', + 3917 => 'ཌྷ', + 3922 => 'དྷ', + 3927 => 'བྷ', + 3932 => 'ཛྷ', + 3945 => 'ཀྵ', + 3955 => 'ཱི', + 3957 => 'ཱུ', + 3958 => 'ྲྀ', + 3959 => 'ྲཱྀ', + 3960 => 'ླྀ', + 3961 => 'ླཱྀ', + 3969 => 'ཱྀ', + 3987 => 'ྒྷ', + 3997 => 'ྜྷ', + 4002 => 'ྡྷ', + 4007 => 'ྦྷ', + 4012 => 'ྫྷ', + 4025 => 'ྐྵ', + 4295 => 'ⴧ', + 4301 => 'ⴭ', + 4348 => 'ნ', + 5112 => 'Ᏸ', + 5113 => 'Ᏹ', + 5114 => 'Ᏺ', + 5115 => 'Ᏻ', + 5116 => 'Ᏼ', + 5117 => 'Ᏽ', + 7296 => 'в', + 7297 => 'д', + 7298 => 'о', + 7299 => 'с', + 7300 => 'т', + 7301 => 'т', + 7302 => 'ъ', + 7303 => 'ѣ', + 7304 => 'ꙋ', + 7312 => 'ა', + 7313 => 'ბ', + 7314 => 'გ', + 7315 => 'დ', + 7316 => 'ე', + 7317 => 'ვ', + 7318 => 'ზ', + 7319 => 'თ', + 7320 => 'ი', + 7321 => 'კ', + 7322 => 'ლ', + 7323 => 'მ', + 7324 => 'ნ', + 7325 => 'ო', + 7326 => 'პ', + 7327 => 'ჟ', + 7328 => 'რ', + 7329 => 'ს', + 7330 => 'ტ', + 7331 => 'უ', + 7332 => 'ფ', + 7333 => 'ქ', + 7334 => 'ღ', + 7335 => 'ყ', + 7336 => 'შ', + 7337 => 'ჩ', + 7338 => 'ც', + 7339 => 'ძ', + 7340 => 'წ', + 7341 => 'ჭ', + 7342 => 'ხ', + 7343 => 'ჯ', + 7344 => 'ჰ', + 7345 => 'ჱ', + 7346 => 'ჲ', + 7347 => 'ჳ', + 7348 => 'ჴ', + 7349 => 'ჵ', + 7350 => 'ჶ', + 7351 => 'ჷ', + 7352 => 'ჸ', + 7353 => 'ჹ', + 7354 => 'ჺ', + 7357 => 'ჽ', + 7358 => 'ჾ', + 7359 => 'ჿ', + 7468 => 'a', + 7469 => 'æ', + 7470 => 'b', + 7472 => 'd', + 7473 => 'e', + 7474 => 'ǝ', + 7475 => 'g', + 7476 => 'h', + 7477 => 'i', + 7478 => 'j', + 7479 => 'k', + 7480 => 'l', + 7481 => 'm', + 7482 => 'n', + 7484 => 'o', + 7485 => 'ȣ', + 7486 => 'p', + 7487 => 'r', + 7488 => 't', + 7489 => 'u', + 7490 => 'w', + 7491 => 'a', + 7492 => 'ɐ', + 7493 => 'ɑ', + 7494 => 'ᴂ', + 7495 => 'b', + 7496 => 'd', + 7497 => 'e', + 7498 => 'ə', + 7499 => 'ɛ', + 7500 => 'ɜ', + 7501 => 'g', + 7503 => 'k', + 7504 => 'm', + 7505 => 'ŋ', + 7506 => 'o', + 7507 => 'ɔ', + 7508 => 'ᴖ', + 7509 => 'ᴗ', + 7510 => 'p', + 7511 => 't', + 7512 => 'u', + 7513 => 'ᴝ', + 7514 => 'ɯ', + 7515 => 'v', + 7516 => 'ᴥ', + 7517 => 'β', + 7518 => 'γ', + 7519 => 'δ', + 7520 => 'φ', + 7521 => 'χ', + 7522 => 'i', + 7523 => 'r', + 7524 => 'u', + 7525 => 'v', + 7526 => 'β', + 7527 => 'γ', + 7528 => 'ρ', + 7529 => 'φ', + 7530 => 'χ', + 7544 => 'н', + 7579 => 'ɒ', + 7580 => 'c', + 7581 => 'ɕ', + 7582 => 'ð', + 7583 => 'ɜ', + 7584 => 'f', + 7585 => 'ɟ', + 7586 => 'ɡ', + 7587 => 'ɥ', + 7588 => 'ɨ', + 7589 => 'ɩ', + 7590 => 'ɪ', + 7591 => 'ᵻ', + 7592 => 'ʝ', + 7593 => 'ɭ', + 7594 => 'ᶅ', + 7595 => 'ʟ', + 7596 => 'ɱ', + 7597 => 'ɰ', + 7598 => 'ɲ', + 7599 => 'ɳ', + 7600 => 'ɴ', + 7601 => 'ɵ', + 7602 => 'ɸ', + 7603 => 'ʂ', + 7604 => 'ʃ', + 7605 => 'ƫ', + 7606 => 'ʉ', + 7607 => 'ʊ', + 7608 => 'ᴜ', + 7609 => 'ʋ', + 7610 => 'ʌ', + 7611 => 'z', + 7612 => 'ʐ', + 7613 => 'ʑ', + 7614 => 'ʒ', + 7615 => 'θ', + 7680 => 'ḁ', + 7682 => 'ḃ', + 7684 => 'ḅ', + 7686 => 'ḇ', + 7688 => 'ḉ', + 7690 => 'ḋ', + 7692 => 'ḍ', + 7694 => 'ḏ', + 7696 => 'ḑ', + 7698 => 'ḓ', + 7700 => 'ḕ', + 7702 => 'ḗ', + 7704 => 'ḙ', + 7706 => 'ḛ', + 7708 => 'ḝ', + 7710 => 'ḟ', + 7712 => 'ḡ', + 7714 => 'ḣ', + 7716 => 'ḥ', + 7718 => 'ḧ', + 7720 => 'ḩ', + 7722 => 'ḫ', + 7724 => 'ḭ', + 7726 => 'ḯ', + 7728 => 'ḱ', + 7730 => 'ḳ', + 7732 => 'ḵ', + 7734 => 'ḷ', + 7736 => 'ḹ', + 7738 => 'ḻ', + 7740 => 'ḽ', + 7742 => 'ḿ', + 7744 => 'ṁ', + 7746 => 'ṃ', + 7748 => 'ṅ', + 7750 => 'ṇ', + 7752 => 'ṉ', + 7754 => 'ṋ', + 7756 => 'ṍ', + 7758 => 'ṏ', + 7760 => 'ṑ', + 7762 => 'ṓ', + 7764 => 'ṕ', + 7766 => 'ṗ', + 7768 => 'ṙ', + 7770 => 'ṛ', + 7772 => 'ṝ', + 7774 => 'ṟ', + 7776 => 'ṡ', + 7778 => 'ṣ', + 7780 => 'ṥ', + 7782 => 'ṧ', + 7784 => 'ṩ', + 7786 => 'ṫ', + 7788 => 'ṭ', + 7790 => 'ṯ', + 7792 => 'ṱ', + 7794 => 'ṳ', + 7796 => 'ṵ', + 7798 => 'ṷ', + 7800 => 'ṹ', + 7802 => 'ṻ', + 7804 => 'ṽ', + 7806 => 'ṿ', + 7808 => 'ẁ', + 7810 => 'ẃ', + 7812 => 'ẅ', + 7814 => 'ẇ', + 7816 => 'ẉ', + 7818 => 'ẋ', + 7820 => 'ẍ', + 7822 => 'ẏ', + 7824 => 'ẑ', + 7826 => 'ẓ', + 7828 => 'ẕ', + 7834 => 'aʾ', + 7835 => 'ṡ', + 7838 => 'ss', + 7840 => 'ạ', + 7842 => 'ả', + 7844 => 'ấ', + 7846 => 'ầ', + 7848 => 'ẩ', + 7850 => 'ẫ', + 7852 => 'ậ', + 7854 => 'ắ', + 7856 => 'ằ', + 7858 => 'ẳ', + 7860 => 'ẵ', + 7862 => 'ặ', + 7864 => 'ẹ', + 7866 => 'ẻ', + 7868 => 'ẽ', + 7870 => 'ế', + 7872 => 'ề', + 7874 => 'ể', + 7876 => 'ễ', + 7878 => 'ệ', + 7880 => 'ỉ', + 7882 => 'ị', + 7884 => 'ọ', + 7886 => 'ỏ', + 7888 => 'ố', + 7890 => 'ồ', + 7892 => 'ổ', + 7894 => 'ỗ', + 7896 => 'ộ', + 7898 => 'ớ', + 7900 => 'ờ', + 7902 => 'ở', + 7904 => 'ỡ', + 7906 => 'ợ', + 7908 => 'ụ', + 7910 => 'ủ', + 7912 => 'ứ', + 7914 => 'ừ', + 7916 => 'ử', + 7918 => 'ữ', + 7920 => 'ự', + 7922 => 'ỳ', + 7924 => 'ỵ', + 7926 => 'ỷ', + 7928 => 'ỹ', + 7930 => 'ỻ', + 7932 => 'ỽ', + 7934 => 'ỿ', + 7944 => 'ἀ', + 7945 => 'ἁ', + 7946 => 'ἂ', + 7947 => 'ἃ', + 7948 => 'ἄ', + 7949 => 'ἅ', + 7950 => 'ἆ', + 7951 => 'ἇ', + 7960 => 'ἐ', + 7961 => 'ἑ', + 7962 => 'ἒ', + 7963 => 'ἓ', + 7964 => 'ἔ', + 7965 => 'ἕ', + 7976 => 'ἠ', + 7977 => 'ἡ', + 7978 => 'ἢ', + 7979 => 'ἣ', + 7980 => 'ἤ', + 7981 => 'ἥ', + 7982 => 'ἦ', + 7983 => 'ἧ', + 7992 => 'ἰ', + 7993 => 'ἱ', + 7994 => 'ἲ', + 7995 => 'ἳ', + 7996 => 'ἴ', + 7997 => 'ἵ', + 7998 => 'ἶ', + 7999 => 'ἷ', + 8008 => 'ὀ', + 8009 => 'ὁ', + 8010 => 'ὂ', + 8011 => 'ὃ', + 8012 => 'ὄ', + 8013 => 'ὅ', + 8025 => 'ὑ', + 8027 => 'ὓ', + 8029 => 'ὕ', + 8031 => 'ὗ', + 8040 => 'ὠ', + 8041 => 'ὡ', + 8042 => 'ὢ', + 8043 => 'ὣ', + 8044 => 'ὤ', + 8045 => 'ὥ', + 8046 => 'ὦ', + 8047 => 'ὧ', + 8049 => 'ά', + 8051 => 'έ', + 8053 => 'ή', + 8055 => 'ί', + 8057 => 'ό', + 8059 => 'ύ', + 8061 => 'ώ', + 8064 => 'ἀι', + 8065 => 'ἁι', + 8066 => 'ἂι', + 8067 => 'ἃι', + 8068 => 'ἄι', + 8069 => 'ἅι', + 8070 => 'ἆι', + 8071 => 'ἇι', + 8072 => 'ἀι', + 8073 => 'ἁι', + 8074 => 'ἂι', + 8075 => 'ἃι', + 8076 => 'ἄι', + 8077 => 'ἅι', + 8078 => 'ἆι', + 8079 => 'ἇι', + 8080 => 'ἠι', + 8081 => 'ἡι', + 8082 => 'ἢι', + 8083 => 'ἣι', + 8084 => 'ἤι', + 8085 => 'ἥι', + 8086 => 'ἦι', + 8087 => 'ἧι', + 8088 => 'ἠι', + 8089 => 'ἡι', + 8090 => 'ἢι', + 8091 => 'ἣι', + 8092 => 'ἤι', + 8093 => 'ἥι', + 8094 => 'ἦι', + 8095 => 'ἧι', + 8096 => 'ὠι', + 8097 => 'ὡι', + 8098 => 'ὢι', + 8099 => 'ὣι', + 8100 => 'ὤι', + 8101 => 'ὥι', + 8102 => 'ὦι', + 8103 => 'ὧι', + 8104 => 'ὠι', + 8105 => 'ὡι', + 8106 => 'ὢι', + 8107 => 'ὣι', + 8108 => 'ὤι', + 8109 => 'ὥι', + 8110 => 'ὦι', + 8111 => 'ὧι', + 8114 => 'ὰι', + 8115 => 'αι', + 8116 => 'άι', + 8119 => 'ᾶι', + 8120 => 'ᾰ', + 8121 => 'ᾱ', + 8122 => 'ὰ', + 8123 => 'ά', + 8124 => 'αι', + 8126 => 'ι', + 8130 => 'ὴι', + 8131 => 'ηι', + 8132 => 'ήι', + 8135 => 'ῆι', + 8136 => 'ὲ', + 8137 => 'έ', + 8138 => 'ὴ', + 8139 => 'ή', + 8140 => 'ηι', + 8147 => 'ΐ', + 8152 => 'ῐ', + 8153 => 'ῑ', + 8154 => 'ὶ', + 8155 => 'ί', + 8163 => 'ΰ', + 8168 => 'ῠ', + 8169 => 'ῡ', + 8170 => 'ὺ', + 8171 => 'ύ', + 8172 => 'ῥ', + 8178 => 'ὼι', + 8179 => 'ωι', + 8180 => 'ώι', + 8183 => 'ῶι', + 8184 => 'ὸ', + 8185 => 'ό', + 8186 => 'ὼ', + 8187 => 'ώ', + 8188 => 'ωι', + 8209 => '‐', + 8243 => '′′', + 8244 => '′′′', + 8246 => '‵‵', + 8247 => '‵‵‵', + 8279 => '′′′′', + 8304 => '0', + 8305 => 'i', + 8308 => '4', + 8309 => '5', + 8310 => '6', + 8311 => '7', + 8312 => '8', + 8313 => '9', + 8315 => '−', + 8319 => 'n', + 8320 => '0', + 8321 => '1', + 8322 => '2', + 8323 => '3', + 8324 => '4', + 8325 => '5', + 8326 => '6', + 8327 => '7', + 8328 => '8', + 8329 => '9', + 8331 => '−', + 8336 => 'a', + 8337 => 'e', + 8338 => 'o', + 8339 => 'x', + 8340 => 'ə', + 8341 => 'h', + 8342 => 'k', + 8343 => 'l', + 8344 => 'm', + 8345 => 'n', + 8346 => 'p', + 8347 => 's', + 8348 => 't', + 8360 => 'rs', + 8450 => 'c', + 8451 => '°c', + 8455 => 'ɛ', + 8457 => '°f', + 8458 => 'g', + 8459 => 'h', + 8460 => 'h', + 8461 => 'h', + 8462 => 'h', + 8463 => 'ħ', + 8464 => 'i', + 8465 => 'i', + 8466 => 'l', + 8467 => 'l', + 8469 => 'n', + 8470 => 'no', + 8473 => 'p', + 8474 => 'q', + 8475 => 'r', + 8476 => 'r', + 8477 => 'r', + 8480 => 'sm', + 8481 => 'tel', + 8482 => 'tm', + 8484 => 'z', + 8486 => 'ω', + 8488 => 'z', + 8490 => 'k', + 8491 => 'å', + 8492 => 'b', + 8493 => 'c', + 8495 => 'e', + 8496 => 'e', + 8497 => 'f', + 8499 => 'm', + 8500 => 'o', + 8501 => 'א', + 8502 => 'ב', + 8503 => 'ג', + 8504 => 'ד', + 8505 => 'i', + 8507 => 'fax', + 8508 => 'π', + 8509 => 'γ', + 8510 => 'γ', + 8511 => 'π', + 8512 => '∑', + 8517 => 'd', + 8518 => 'd', + 8519 => 'e', + 8520 => 'i', + 8521 => 'j', + 8528 => '1⁄7', + 8529 => '1⁄9', + 8530 => '1⁄10', + 8531 => '1⁄3', + 8532 => '2⁄3', + 8533 => '1⁄5', + 8534 => '2⁄5', + 8535 => '3⁄5', + 8536 => '4⁄5', + 8537 => '1⁄6', + 8538 => '5⁄6', + 8539 => '1⁄8', + 8540 => '3⁄8', + 8541 => '5⁄8', + 8542 => '7⁄8', + 8543 => '1⁄', + 8544 => 'i', + 8545 => 'ii', + 8546 => 'iii', + 8547 => 'iv', + 8548 => 'v', + 8549 => 'vi', + 8550 => 'vii', + 8551 => 'viii', + 8552 => 'ix', + 8553 => 'x', + 8554 => 'xi', + 8555 => 'xii', + 8556 => 'l', + 8557 => 'c', + 8558 => 'd', + 8559 => 'm', + 8560 => 'i', + 8561 => 'ii', + 8562 => 'iii', + 8563 => 'iv', + 8564 => 'v', + 8565 => 'vi', + 8566 => 'vii', + 8567 => 'viii', + 8568 => 'ix', + 8569 => 'x', + 8570 => 'xi', + 8571 => 'xii', + 8572 => 'l', + 8573 => 'c', + 8574 => 'd', + 8575 => 'm', + 8585 => '0⁄3', + 8748 => '∫∫', + 8749 => '∫∫∫', + 8751 => '∮∮', + 8752 => '∮∮∮', + 9001 => '〈', + 9002 => '〉', + 9312 => '1', + 9313 => '2', + 9314 => '3', + 9315 => '4', + 9316 => '5', + 9317 => '6', + 9318 => '7', + 9319 => '8', + 9320 => '9', + 9321 => '10', + 9322 => '11', + 9323 => '12', + 9324 => '13', + 9325 => '14', + 9326 => '15', + 9327 => '16', + 9328 => '17', + 9329 => '18', + 9330 => '19', + 9331 => '20', + 9398 => 'a', + 9399 => 'b', + 9400 => 'c', + 9401 => 'd', + 9402 => 'e', + 9403 => 'f', + 9404 => 'g', + 9405 => 'h', + 9406 => 'i', + 9407 => 'j', + 9408 => 'k', + 9409 => 'l', + 9410 => 'm', + 9411 => 'n', + 9412 => 'o', + 9413 => 'p', + 9414 => 'q', + 9415 => 'r', + 9416 => 's', + 9417 => 't', + 9418 => 'u', + 9419 => 'v', + 9420 => 'w', + 9421 => 'x', + 9422 => 'y', + 9423 => 'z', + 9424 => 'a', + 9425 => 'b', + 9426 => 'c', + 9427 => 'd', + 9428 => 'e', + 9429 => 'f', + 9430 => 'g', + 9431 => 'h', + 9432 => 'i', + 9433 => 'j', + 9434 => 'k', + 9435 => 'l', + 9436 => 'm', + 9437 => 'n', + 9438 => 'o', + 9439 => 'p', + 9440 => 'q', + 9441 => 'r', + 9442 => 's', + 9443 => 't', + 9444 => 'u', + 9445 => 'v', + 9446 => 'w', + 9447 => 'x', + 9448 => 'y', + 9449 => 'z', + 9450 => '0', + 10764 => '∫∫∫∫', + 10972 => '⫝̸', + 11264 => 'ⰰ', + 11265 => 'ⰱ', + 11266 => 'ⰲ', + 11267 => 'ⰳ', + 11268 => 'ⰴ', + 11269 => 'ⰵ', + 11270 => 'ⰶ', + 11271 => 'ⰷ', + 11272 => 'ⰸ', + 11273 => 'ⰹ', + 11274 => 'ⰺ', + 11275 => 'ⰻ', + 11276 => 'ⰼ', + 11277 => 'ⰽ', + 11278 => 'ⰾ', + 11279 => 'ⰿ', + 11280 => 'ⱀ', + 11281 => 'ⱁ', + 11282 => 'ⱂ', + 11283 => 'ⱃ', + 11284 => 'ⱄ', + 11285 => 'ⱅ', + 11286 => 'ⱆ', + 11287 => 'ⱇ', + 11288 => 'ⱈ', + 11289 => 'ⱉ', + 11290 => 'ⱊ', + 11291 => 'ⱋ', + 11292 => 'ⱌ', + 11293 => 'ⱍ', + 11294 => 'ⱎ', + 11295 => 'ⱏ', + 11296 => 'ⱐ', + 11297 => 'ⱑ', + 11298 => 'ⱒ', + 11299 => 'ⱓ', + 11300 => 'ⱔ', + 11301 => 'ⱕ', + 11302 => 'ⱖ', + 11303 => 'ⱗ', + 11304 => 'ⱘ', + 11305 => 'ⱙ', + 11306 => 'ⱚ', + 11307 => 'ⱛ', + 11308 => 'ⱜ', + 11309 => 'ⱝ', + 11310 => 'ⱞ', + 11360 => 'ⱡ', + 11362 => 'ɫ', + 11363 => 'ᵽ', + 11364 => 'ɽ', + 11367 => 'ⱨ', + 11369 => 'ⱪ', + 11371 => 'ⱬ', + 11373 => 'ɑ', + 11374 => 'ɱ', + 11375 => 'ɐ', + 11376 => 'ɒ', + 11378 => 'ⱳ', + 11381 => 'ⱶ', + 11388 => 'j', + 11389 => 'v', + 11390 => 'ȿ', + 11391 => 'ɀ', + 11392 => 'ⲁ', + 11394 => 'ⲃ', + 11396 => 'ⲅ', + 11398 => 'ⲇ', + 11400 => 'ⲉ', + 11402 => 'ⲋ', + 11404 => 'ⲍ', + 11406 => 'ⲏ', + 11408 => 'ⲑ', + 11410 => 'ⲓ', + 11412 => 'ⲕ', + 11414 => 'ⲗ', + 11416 => 'ⲙ', + 11418 => 'ⲛ', + 11420 => 'ⲝ', + 11422 => 'ⲟ', + 11424 => 'ⲡ', + 11426 => 'ⲣ', + 11428 => 'ⲥ', + 11430 => 'ⲧ', + 11432 => 'ⲩ', + 11434 => 'ⲫ', + 11436 => 'ⲭ', + 11438 => 'ⲯ', + 11440 => 'ⲱ', + 11442 => 'ⲳ', + 11444 => 'ⲵ', + 11446 => 'ⲷ', + 11448 => 'ⲹ', + 11450 => 'ⲻ', + 11452 => 'ⲽ', + 11454 => 'ⲿ', + 11456 => 'ⳁ', + 11458 => 'ⳃ', + 11460 => 'ⳅ', + 11462 => 'ⳇ', + 11464 => 'ⳉ', + 11466 => 'ⳋ', + 11468 => 'ⳍ', + 11470 => 'ⳏ', + 11472 => 'ⳑ', + 11474 => 'ⳓ', + 11476 => 'ⳕ', + 11478 => 'ⳗ', + 11480 => 'ⳙ', + 11482 => 'ⳛ', + 11484 => 'ⳝ', + 11486 => 'ⳟ', + 11488 => 'ⳡ', + 11490 => 'ⳣ', + 11499 => 'ⳬ', + 11501 => 'ⳮ', + 11506 => 'ⳳ', + 11631 => 'ⵡ', + 11935 => '母', + 12019 => '龟', + 12032 => '一', + 12033 => '丨', + 12034 => '丶', + 12035 => '丿', + 12036 => '乙', + 12037 => '亅', + 12038 => '二', + 12039 => '亠', + 12040 => '人', + 12041 => '儿', + 12042 => '入', + 12043 => '八', + 12044 => '冂', + 12045 => '冖', + 12046 => '冫', + 12047 => '几', + 12048 => '凵', + 12049 => '刀', + 12050 => '力', + 12051 => '勹', + 12052 => '匕', + 12053 => '匚', + 12054 => '匸', + 12055 => '十', + 12056 => '卜', + 12057 => '卩', + 12058 => '厂', + 12059 => '厶', + 12060 => '又', + 12061 => '口', + 12062 => '囗', + 12063 => '土', + 12064 => '士', + 12065 => '夂', + 12066 => '夊', + 12067 => '夕', + 12068 => '大', + 12069 => '女', + 12070 => '子', + 12071 => '宀', + 12072 => '寸', + 12073 => '小', + 12074 => '尢', + 12075 => '尸', + 12076 => '屮', + 12077 => '山', + 12078 => '巛', + 12079 => '工', + 12080 => '己', + 12081 => '巾', + 12082 => '干', + 12083 => '幺', + 12084 => '广', + 12085 => '廴', + 12086 => '廾', + 12087 => '弋', + 12088 => '弓', + 12089 => '彐', + 12090 => '彡', + 12091 => '彳', + 12092 => '心', + 12093 => '戈', + 12094 => '戶', + 12095 => '手', + 12096 => '支', + 12097 => '攴', + 12098 => '文', + 12099 => '斗', + 12100 => '斤', + 12101 => '方', + 12102 => '无', + 12103 => '日', + 12104 => '曰', + 12105 => '月', + 12106 => '木', + 12107 => '欠', + 12108 => '止', + 12109 => '歹', + 12110 => '殳', + 12111 => '毋', + 12112 => '比', + 12113 => '毛', + 12114 => '氏', + 12115 => '气', + 12116 => '水', + 12117 => '火', + 12118 => '爪', + 12119 => '父', + 12120 => '爻', + 12121 => '爿', + 12122 => '片', + 12123 => '牙', + 12124 => '牛', + 12125 => '犬', + 12126 => '玄', + 12127 => '玉', + 12128 => '瓜', + 12129 => '瓦', + 12130 => '甘', + 12131 => '生', + 12132 => '用', + 12133 => '田', + 12134 => '疋', + 12135 => '疒', + 12136 => '癶', + 12137 => '白', + 12138 => '皮', + 12139 => '皿', + 12140 => '目', + 12141 => '矛', + 12142 => '矢', + 12143 => '石', + 12144 => '示', + 12145 => '禸', + 12146 => '禾', + 12147 => '穴', + 12148 => '立', + 12149 => '竹', + 12150 => '米', + 12151 => '糸', + 12152 => '缶', + 12153 => '网', + 12154 => '羊', + 12155 => '羽', + 12156 => '老', + 12157 => '而', + 12158 => '耒', + 12159 => '耳', + 12160 => '聿', + 12161 => '肉', + 12162 => '臣', + 12163 => '自', + 12164 => '至', + 12165 => '臼', + 12166 => '舌', + 12167 => '舛', + 12168 => '舟', + 12169 => '艮', + 12170 => '色', + 12171 => '艸', + 12172 => '虍', + 12173 => '虫', + 12174 => '血', + 12175 => '行', + 12176 => '衣', + 12177 => '襾', + 12178 => '見', + 12179 => '角', + 12180 => '言', + 12181 => '谷', + 12182 => '豆', + 12183 => '豕', + 12184 => '豸', + 12185 => '貝', + 12186 => '赤', + 12187 => '走', + 12188 => '足', + 12189 => '身', + 12190 => '車', + 12191 => '辛', + 12192 => '辰', + 12193 => '辵', + 12194 => '邑', + 12195 => '酉', + 12196 => '釆', + 12197 => '里', + 12198 => '金', + 12199 => '長', + 12200 => '門', + 12201 => '阜', + 12202 => '隶', + 12203 => '隹', + 12204 => '雨', + 12205 => '靑', + 12206 => '非', + 12207 => '面', + 12208 => '革', + 12209 => '韋', + 12210 => '韭', + 12211 => '音', + 12212 => '頁', + 12213 => '風', + 12214 => '飛', + 12215 => '食', + 12216 => '首', + 12217 => '香', + 12218 => '馬', + 12219 => '骨', + 12220 => '高', + 12221 => '髟', + 12222 => '鬥', + 12223 => '鬯', + 12224 => '鬲', + 12225 => '鬼', + 12226 => '魚', + 12227 => '鳥', + 12228 => '鹵', + 12229 => '鹿', + 12230 => '麥', + 12231 => '麻', + 12232 => '黃', + 12233 => '黍', + 12234 => '黑', + 12235 => '黹', + 12236 => '黽', + 12237 => '鼎', + 12238 => '鼓', + 12239 => '鼠', + 12240 => '鼻', + 12241 => '齊', + 12242 => '齒', + 12243 => '龍', + 12244 => '龜', + 12245 => '龠', + 12290 => '.', + 12342 => '〒', + 12344 => '十', + 12345 => '卄', + 12346 => '卅', + 12447 => 'より', + 12543 => 'コト', + 12593 => 'ᄀ', + 12594 => 'ᄁ', + 12595 => 'ᆪ', + 12596 => 'ᄂ', + 12597 => 'ᆬ', + 12598 => 'ᆭ', + 12599 => 'ᄃ', + 12600 => 'ᄄ', + 12601 => 'ᄅ', + 12602 => 'ᆰ', + 12603 => 'ᆱ', + 12604 => 'ᆲ', + 12605 => 'ᆳ', + 12606 => 'ᆴ', + 12607 => 'ᆵ', + 12608 => 'ᄚ', + 12609 => 'ᄆ', + 12610 => 'ᄇ', + 12611 => 'ᄈ', + 12612 => 'ᄡ', + 12613 => 'ᄉ', + 12614 => 'ᄊ', + 12615 => 'ᄋ', + 12616 => 'ᄌ', + 12617 => 'ᄍ', + 12618 => 'ᄎ', + 12619 => 'ᄏ', + 12620 => 'ᄐ', + 12621 => 'ᄑ', + 12622 => 'ᄒ', + 12623 => 'ᅡ', + 12624 => 'ᅢ', + 12625 => 'ᅣ', + 12626 => 'ᅤ', + 12627 => 'ᅥ', + 12628 => 'ᅦ', + 12629 => 'ᅧ', + 12630 => 'ᅨ', + 12631 => 'ᅩ', + 12632 => 'ᅪ', + 12633 => 'ᅫ', + 12634 => 'ᅬ', + 12635 => 'ᅭ', + 12636 => 'ᅮ', + 12637 => 'ᅯ', + 12638 => 'ᅰ', + 12639 => 'ᅱ', + 12640 => 'ᅲ', + 12641 => 'ᅳ', + 12642 => 'ᅴ', + 12643 => 'ᅵ', + 12645 => 'ᄔ', + 12646 => 'ᄕ', + 12647 => 'ᇇ', + 12648 => 'ᇈ', + 12649 => 'ᇌ', + 12650 => 'ᇎ', + 12651 => 'ᇓ', + 12652 => 'ᇗ', + 12653 => 'ᇙ', + 12654 => 'ᄜ', + 12655 => 'ᇝ', + 12656 => 'ᇟ', + 12657 => 'ᄝ', + 12658 => 'ᄞ', + 12659 => 'ᄠ', + 12660 => 'ᄢ', + 12661 => 'ᄣ', + 12662 => 'ᄧ', + 12663 => 'ᄩ', + 12664 => 'ᄫ', + 12665 => 'ᄬ', + 12666 => 'ᄭ', + 12667 => 'ᄮ', + 12668 => 'ᄯ', + 12669 => 'ᄲ', + 12670 => 'ᄶ', + 12671 => 'ᅀ', + 12672 => 'ᅇ', + 12673 => 'ᅌ', + 12674 => 'ᇱ', + 12675 => 'ᇲ', + 12676 => 'ᅗ', + 12677 => 'ᅘ', + 12678 => 'ᅙ', + 12679 => 'ᆄ', + 12680 => 'ᆅ', + 12681 => 'ᆈ', + 12682 => 'ᆑ', + 12683 => 'ᆒ', + 12684 => 'ᆔ', + 12685 => 'ᆞ', + 12686 => 'ᆡ', + 12690 => '一', + 12691 => '二', + 12692 => '三', + 12693 => '四', + 12694 => '上', + 12695 => '中', + 12696 => '下', + 12697 => '甲', + 12698 => '乙', + 12699 => '丙', + 12700 => '丁', + 12701 => '天', + 12702 => '地', + 12703 => '人', + 12868 => '問', + 12869 => '幼', + 12870 => '文', + 12871 => '箏', + 12880 => 'pte', + 12881 => '21', + 12882 => '22', + 12883 => '23', + 12884 => '24', + 12885 => '25', + 12886 => '26', + 12887 => '27', + 12888 => '28', + 12889 => '29', + 12890 => '30', + 12891 => '31', + 12892 => '32', + 12893 => '33', + 12894 => '34', + 12895 => '35', + 12896 => 'ᄀ', + 12897 => 'ᄂ', + 12898 => 'ᄃ', + 12899 => 'ᄅ', + 12900 => 'ᄆ', + 12901 => 'ᄇ', + 12902 => 'ᄉ', + 12903 => 'ᄋ', + 12904 => 'ᄌ', + 12905 => 'ᄎ', + 12906 => 'ᄏ', + 12907 => 'ᄐ', + 12908 => 'ᄑ', + 12909 => 'ᄒ', + 12910 => '가', + 12911 => '나', + 12912 => '다', + 12913 => '라', + 12914 => '마', + 12915 => '바', + 12916 => '사', + 12917 => '아', + 12918 => '자', + 12919 => '차', + 12920 => '카', + 12921 => '타', + 12922 => '파', + 12923 => '하', + 12924 => '참고', + 12925 => '주의', + 12926 => '우', + 12928 => '一', + 12929 => '二', + 12930 => '三', + 12931 => '四', + 12932 => '五', + 12933 => '六', + 12934 => '七', + 12935 => '八', + 12936 => '九', + 12937 => '十', + 12938 => '月', + 12939 => '火', + 12940 => '水', + 12941 => '木', + 12942 => '金', + 12943 => '土', + 12944 => '日', + 12945 => '株', + 12946 => '有', + 12947 => '社', + 12948 => '名', + 12949 => '特', + 12950 => '財', + 12951 => '祝', + 12952 => '労', + 12953 => '秘', + 12954 => '男', + 12955 => '女', + 12956 => '適', + 12957 => '優', + 12958 => '印', + 12959 => '注', + 12960 => '項', + 12961 => '休', + 12962 => '写', + 12963 => '正', + 12964 => '上', + 12965 => '中', + 12966 => '下', + 12967 => '左', + 12968 => '右', + 12969 => '医', + 12970 => '宗', + 12971 => '学', + 12972 => '監', + 12973 => '企', + 12974 => '資', + 12975 => '協', + 12976 => '夜', + 12977 => '36', + 12978 => '37', + 12979 => '38', + 12980 => '39', + 12981 => '40', + 12982 => '41', + 12983 => '42', + 12984 => '43', + 12985 => '44', + 12986 => '45', + 12987 => '46', + 12988 => '47', + 12989 => '48', + 12990 => '49', + 12991 => '50', + 12992 => '1月', + 12993 => '2月', + 12994 => '3月', + 12995 => '4月', + 12996 => '5月', + 12997 => '6月', + 12998 => '7月', + 12999 => '8月', + 13000 => '9月', + 13001 => '10月', + 13002 => '11月', + 13003 => '12月', + 13004 => 'hg', + 13005 => 'erg', + 13006 => 'ev', + 13007 => 'ltd', + 13008 => 'ア', + 13009 => 'イ', + 13010 => 'ウ', + 13011 => 'エ', + 13012 => 'オ', + 13013 => 'カ', + 13014 => 'キ', + 13015 => 'ク', + 13016 => 'ケ', + 13017 => 'コ', + 13018 => 'サ', + 13019 => 'シ', + 13020 => 'ス', + 13021 => 'セ', + 13022 => 'ソ', + 13023 => 'タ', + 13024 => 'チ', + 13025 => 'ツ', + 13026 => 'テ', + 13027 => 'ト', + 13028 => 'ナ', + 13029 => 'ニ', + 13030 => 'ヌ', + 13031 => 'ネ', + 13032 => 'ノ', + 13033 => 'ハ', + 13034 => 'ヒ', + 13035 => 'フ', + 13036 => 'ヘ', + 13037 => 'ホ', + 13038 => 'マ', + 13039 => 'ミ', + 13040 => 'ム', + 13041 => 'メ', + 13042 => 'モ', + 13043 => 'ヤ', + 13044 => 'ユ', + 13045 => 'ヨ', + 13046 => 'ラ', + 13047 => 'リ', + 13048 => 'ル', + 13049 => 'レ', + 13050 => 'ロ', + 13051 => 'ワ', + 13052 => 'ヰ', + 13053 => 'ヱ', + 13054 => 'ヲ', + 13055 => '令和', + 13056 => 'アパート', + 13057 => 'アルファ', + 13058 => 'アンペア', + 13059 => 'アール', + 13060 => 'イニング', + 13061 => 'インチ', + 13062 => 'ウォン', + 13063 => 'エスクード', + 13064 => 'エーカー', + 13065 => 'オンス', + 13066 => 'オーム', + 13067 => 'カイリ', + 13068 => 'カラット', + 13069 => 'カロリー', + 13070 => 'ガロン', + 13071 => 'ガンマ', + 13072 => 'ギガ', + 13073 => 'ギニー', + 13074 => 'キュリー', + 13075 => 'ギルダー', + 13076 => 'キロ', + 13077 => 'キログラム', + 13078 => 'キロメートル', + 13079 => 'キロワット', + 13080 => 'グラム', + 13081 => 'グラムトン', + 13082 => 'クルゼイロ', + 13083 => 'クローネ', + 13084 => 'ケース', + 13085 => 'コルナ', + 13086 => 'コーポ', + 13087 => 'サイクル', + 13088 => 'サンチーム', + 13089 => 'シリング', + 13090 => 'センチ', + 13091 => 'セント', + 13092 => 'ダース', + 13093 => 'デシ', + 13094 => 'ドル', + 13095 => 'トン', + 13096 => 'ナノ', + 13097 => 'ノット', + 13098 => 'ハイツ', + 13099 => 'パーセント', + 13100 => 'パーツ', + 13101 => 'バーレル', + 13102 => 'ピアストル', + 13103 => 'ピクル', + 13104 => 'ピコ', + 13105 => 'ビル', + 13106 => 'ファラッド', + 13107 => 'フィート', + 13108 => 'ブッシェル', + 13109 => 'フラン', + 13110 => 'ヘクタール', + 13111 => 'ペソ', + 13112 => 'ペニヒ', + 13113 => 'ヘルツ', + 13114 => 'ペンス', + 13115 => 'ページ', + 13116 => 'ベータ', + 13117 => 'ポイント', + 13118 => 'ボルト', + 13119 => 'ホン', + 13120 => 'ポンド', + 13121 => 'ホール', + 13122 => 'ホーン', + 13123 => 'マイクロ', + 13124 => 'マイル', + 13125 => 'マッハ', + 13126 => 'マルク', + 13127 => 'マンション', + 13128 => 'ミクロン', + 13129 => 'ミリ', + 13130 => 'ミリバール', + 13131 => 'メガ', + 13132 => 'メガトン', + 13133 => 'メートル', + 13134 => 'ヤード', + 13135 => 'ヤール', + 13136 => 'ユアン', + 13137 => 'リットル', + 13138 => 'リラ', + 13139 => 'ルピー', + 13140 => 'ルーブル', + 13141 => 'レム', + 13142 => 'レントゲン', + 13143 => 'ワット', + 13144 => '0点', + 13145 => '1点', + 13146 => '2点', + 13147 => '3点', + 13148 => '4点', + 13149 => '5点', + 13150 => '6点', + 13151 => '7点', + 13152 => '8点', + 13153 => '9点', + 13154 => '10点', + 13155 => '11点', + 13156 => '12点', + 13157 => '13点', + 13158 => '14点', + 13159 => '15点', + 13160 => '16点', + 13161 => '17点', + 13162 => '18点', + 13163 => '19点', + 13164 => '20点', + 13165 => '21点', + 13166 => '22点', + 13167 => '23点', + 13168 => '24点', + 13169 => 'hpa', + 13170 => 'da', + 13171 => 'au', + 13172 => 'bar', + 13173 => 'ov', + 13174 => 'pc', + 13175 => 'dm', + 13176 => 'dm2', + 13177 => 'dm3', + 13178 => 'iu', + 13179 => '平成', + 13180 => '昭和', + 13181 => '大正', + 13182 => '明治', + 13183 => '株式会社', + 13184 => 'pa', + 13185 => 'na', + 13186 => 'μa', + 13187 => 'ma', + 13188 => 'ka', + 13189 => 'kb', + 13190 => 'mb', + 13191 => 'gb', + 13192 => 'cal', + 13193 => 'kcal', + 13194 => 'pf', + 13195 => 'nf', + 13196 => 'μf', + 13197 => 'μg', + 13198 => 'mg', + 13199 => 'kg', + 13200 => 'hz', + 13201 => 'khz', + 13202 => 'mhz', + 13203 => 'ghz', + 13204 => 'thz', + 13205 => 'μl', + 13206 => 'ml', + 13207 => 'dl', + 13208 => 'kl', + 13209 => 'fm', + 13210 => 'nm', + 13211 => 'μm', + 13212 => 'mm', + 13213 => 'cm', + 13214 => 'km', + 13215 => 'mm2', + 13216 => 'cm2', + 13217 => 'm2', + 13218 => 'km2', + 13219 => 'mm3', + 13220 => 'cm3', + 13221 => 'm3', + 13222 => 'km3', + 13223 => 'm∕s', + 13224 => 'm∕s2', + 13225 => 'pa', + 13226 => 'kpa', + 13227 => 'mpa', + 13228 => 'gpa', + 13229 => 'rad', + 13230 => 'rad∕s', + 13231 => 'rad∕s2', + 13232 => 'ps', + 13233 => 'ns', + 13234 => 'μs', + 13235 => 'ms', + 13236 => 'pv', + 13237 => 'nv', + 13238 => 'μv', + 13239 => 'mv', + 13240 => 'kv', + 13241 => 'mv', + 13242 => 'pw', + 13243 => 'nw', + 13244 => 'μw', + 13245 => 'mw', + 13246 => 'kw', + 13247 => 'mw', + 13248 => 'kω', + 13249 => 'mω', + 13251 => 'bq', + 13252 => 'cc', + 13253 => 'cd', + 13254 => 'c∕kg', + 13256 => 'db', + 13257 => 'gy', + 13258 => 'ha', + 13259 => 'hp', + 13260 => 'in', + 13261 => 'kk', + 13262 => 'km', + 13263 => 'kt', + 13264 => 'lm', + 13265 => 'ln', + 13266 => 'log', + 13267 => 'lx', + 13268 => 'mb', + 13269 => 'mil', + 13270 => 'mol', + 13271 => 'ph', + 13273 => 'ppm', + 13274 => 'pr', + 13275 => 'sr', + 13276 => 'sv', + 13277 => 'wb', + 13278 => 'v∕m', + 13279 => 'a∕m', + 13280 => '1日', + 13281 => '2日', + 13282 => '3日', + 13283 => '4日', + 13284 => '5日', + 13285 => '6日', + 13286 => '7日', + 13287 => '8日', + 13288 => '9日', + 13289 => '10日', + 13290 => '11日', + 13291 => '12日', + 13292 => '13日', + 13293 => '14日', + 13294 => '15日', + 13295 => '16日', + 13296 => '17日', + 13297 => '18日', + 13298 => '19日', + 13299 => '20日', + 13300 => '21日', + 13301 => '22日', + 13302 => '23日', + 13303 => '24日', + 13304 => '25日', + 13305 => '26日', + 13306 => '27日', + 13307 => '28日', + 13308 => '29日', + 13309 => '30日', + 13310 => '31日', + 13311 => 'gal', + 42560 => 'ꙁ', + 42562 => 'ꙃ', + 42564 => 'ꙅ', + 42566 => 'ꙇ', + 42568 => 'ꙉ', + 42570 => 'ꙋ', + 42572 => 'ꙍ', + 42574 => 'ꙏ', + 42576 => 'ꙑ', + 42578 => 'ꙓ', + 42580 => 'ꙕ', + 42582 => 'ꙗ', + 42584 => 'ꙙ', + 42586 => 'ꙛ', + 42588 => 'ꙝ', + 42590 => 'ꙟ', + 42592 => 'ꙡ', + 42594 => 'ꙣ', + 42596 => 'ꙥ', + 42598 => 'ꙧ', + 42600 => 'ꙩ', + 42602 => 'ꙫ', + 42604 => 'ꙭ', + 42624 => 'ꚁ', + 42626 => 'ꚃ', + 42628 => 'ꚅ', + 42630 => 'ꚇ', + 42632 => 'ꚉ', + 42634 => 'ꚋ', + 42636 => 'ꚍ', + 42638 => 'ꚏ', + 42640 => 'ꚑ', + 42642 => 'ꚓ', + 42644 => 'ꚕ', + 42646 => 'ꚗ', + 42648 => 'ꚙ', + 42650 => 'ꚛ', + 42652 => 'ъ', + 42653 => 'ь', + 42786 => 'ꜣ', + 42788 => 'ꜥ', + 42790 => 'ꜧ', + 42792 => 'ꜩ', + 42794 => 'ꜫ', + 42796 => 'ꜭ', + 42798 => 'ꜯ', + 42802 => 'ꜳ', + 42804 => 'ꜵ', + 42806 => 'ꜷ', + 42808 => 'ꜹ', + 42810 => 'ꜻ', + 42812 => 'ꜽ', + 42814 => 'ꜿ', + 42816 => 'ꝁ', + 42818 => 'ꝃ', + 42820 => 'ꝅ', + 42822 => 'ꝇ', + 42824 => 'ꝉ', + 42826 => 'ꝋ', + 42828 => 'ꝍ', + 42830 => 'ꝏ', + 42832 => 'ꝑ', + 42834 => 'ꝓ', + 42836 => 'ꝕ', + 42838 => 'ꝗ', + 42840 => 'ꝙ', + 42842 => 'ꝛ', + 42844 => 'ꝝ', + 42846 => 'ꝟ', + 42848 => 'ꝡ', + 42850 => 'ꝣ', + 42852 => 'ꝥ', + 42854 => 'ꝧ', + 42856 => 'ꝩ', + 42858 => 'ꝫ', + 42860 => 'ꝭ', + 42862 => 'ꝯ', + 42864 => 'ꝯ', + 42873 => 'ꝺ', + 42875 => 'ꝼ', + 42877 => 'ᵹ', + 42878 => 'ꝿ', + 42880 => 'ꞁ', + 42882 => 'ꞃ', + 42884 => 'ꞅ', + 42886 => 'ꞇ', + 42891 => 'ꞌ', + 42893 => 'ɥ', + 42896 => 'ꞑ', + 42898 => 'ꞓ', + 42902 => 'ꞗ', + 42904 => 'ꞙ', + 42906 => 'ꞛ', + 42908 => 'ꞝ', + 42910 => 'ꞟ', + 42912 => 'ꞡ', + 42914 => 'ꞣ', + 42916 => 'ꞥ', + 42918 => 'ꞧ', + 42920 => 'ꞩ', + 42922 => 'ɦ', + 42923 => 'ɜ', + 42924 => 'ɡ', + 42925 => 'ɬ', + 42926 => 'ɪ', + 42928 => 'ʞ', + 42929 => 'ʇ', + 42930 => 'ʝ', + 42931 => 'ꭓ', + 42932 => 'ꞵ', + 42934 => 'ꞷ', + 42936 => 'ꞹ', + 42938 => 'ꞻ', + 42940 => 'ꞽ', + 42942 => 'ꞿ', + 42946 => 'ꟃ', + 42948 => 'ꞔ', + 42949 => 'ʂ', + 42950 => 'ᶎ', + 42951 => 'ꟈ', + 42953 => 'ꟊ', + 42997 => 'ꟶ', + 43000 => 'ħ', + 43001 => 'œ', + 43868 => 'ꜧ', + 43869 => 'ꬷ', + 43870 => 'ɫ', + 43871 => 'ꭒ', + 43881 => 'ʍ', + 43888 => 'Ꭰ', + 43889 => 'Ꭱ', + 43890 => 'Ꭲ', + 43891 => 'Ꭳ', + 43892 => 'Ꭴ', + 43893 => 'Ꭵ', + 43894 => 'Ꭶ', + 43895 => 'Ꭷ', + 43896 => 'Ꭸ', + 43897 => 'Ꭹ', + 43898 => 'Ꭺ', + 43899 => 'Ꭻ', + 43900 => 'Ꭼ', + 43901 => 'Ꭽ', + 43902 => 'Ꭾ', + 43903 => 'Ꭿ', + 43904 => 'Ꮀ', + 43905 => 'Ꮁ', + 43906 => 'Ꮂ', + 43907 => 'Ꮃ', + 43908 => 'Ꮄ', + 43909 => 'Ꮅ', + 43910 => 'Ꮆ', + 43911 => 'Ꮇ', + 43912 => 'Ꮈ', + 43913 => 'Ꮉ', + 43914 => 'Ꮊ', + 43915 => 'Ꮋ', + 43916 => 'Ꮌ', + 43917 => 'Ꮍ', + 43918 => 'Ꮎ', + 43919 => 'Ꮏ', + 43920 => 'Ꮐ', + 43921 => 'Ꮑ', + 43922 => 'Ꮒ', + 43923 => 'Ꮓ', + 43924 => 'Ꮔ', + 43925 => 'Ꮕ', + 43926 => 'Ꮖ', + 43927 => 'Ꮗ', + 43928 => 'Ꮘ', + 43929 => 'Ꮙ', + 43930 => 'Ꮚ', + 43931 => 'Ꮛ', + 43932 => 'Ꮜ', + 43933 => 'Ꮝ', + 43934 => 'Ꮞ', + 43935 => 'Ꮟ', + 43936 => 'Ꮠ', + 43937 => 'Ꮡ', + 43938 => 'Ꮢ', + 43939 => 'Ꮣ', + 43940 => 'Ꮤ', + 43941 => 'Ꮥ', + 43942 => 'Ꮦ', + 43943 => 'Ꮧ', + 43944 => 'Ꮨ', + 43945 => 'Ꮩ', + 43946 => 'Ꮪ', + 43947 => 'Ꮫ', + 43948 => 'Ꮬ', + 43949 => 'Ꮭ', + 43950 => 'Ꮮ', + 43951 => 'Ꮯ', + 43952 => 'Ꮰ', + 43953 => 'Ꮱ', + 43954 => 'Ꮲ', + 43955 => 'Ꮳ', + 43956 => 'Ꮴ', + 43957 => 'Ꮵ', + 43958 => 'Ꮶ', + 43959 => 'Ꮷ', + 43960 => 'Ꮸ', + 43961 => 'Ꮹ', + 43962 => 'Ꮺ', + 43963 => 'Ꮻ', + 43964 => 'Ꮼ', + 43965 => 'Ꮽ', + 43966 => 'Ꮾ', + 43967 => 'Ꮿ', + 63744 => '豈', + 63745 => '更', + 63746 => '車', + 63747 => '賈', + 63748 => '滑', + 63749 => '串', + 63750 => '句', + 63751 => '龜', + 63752 => '龜', + 63753 => '契', + 63754 => '金', + 63755 => '喇', + 63756 => '奈', + 63757 => '懶', + 63758 => '癩', + 63759 => '羅', + 63760 => '蘿', + 63761 => '螺', + 63762 => '裸', + 63763 => '邏', + 63764 => '樂', + 63765 => '洛', + 63766 => '烙', + 63767 => '珞', + 63768 => '落', + 63769 => '酪', + 63770 => '駱', + 63771 => '亂', + 63772 => '卵', + 63773 => '欄', + 63774 => '爛', + 63775 => '蘭', + 63776 => '鸞', + 63777 => '嵐', + 63778 => '濫', + 63779 => '藍', + 63780 => '襤', + 63781 => '拉', + 63782 => '臘', + 63783 => '蠟', + 63784 => '廊', + 63785 => '朗', + 63786 => '浪', + 63787 => '狼', + 63788 => '郎', + 63789 => '來', + 63790 => '冷', + 63791 => '勞', + 63792 => '擄', + 63793 => '櫓', + 63794 => '爐', + 63795 => '盧', + 63796 => '老', + 63797 => '蘆', + 63798 => '虜', + 63799 => '路', + 63800 => '露', + 63801 => '魯', + 63802 => '鷺', + 63803 => '碌', + 63804 => '祿', + 63805 => '綠', + 63806 => '菉', + 63807 => '錄', + 63808 => '鹿', + 63809 => '論', + 63810 => '壟', + 63811 => '弄', + 63812 => '籠', + 63813 => '聾', + 63814 => '牢', + 63815 => '磊', + 63816 => '賂', + 63817 => '雷', + 63818 => '壘', + 63819 => '屢', + 63820 => '樓', + 63821 => '淚', + 63822 => '漏', + 63823 => '累', + 63824 => '縷', + 63825 => '陋', + 63826 => '勒', + 63827 => '肋', + 63828 => '凜', + 63829 => '凌', + 63830 => '稜', + 63831 => '綾', + 63832 => '菱', + 63833 => '陵', + 63834 => '讀', + 63835 => '拏', + 63836 => '樂', + 63837 => '諾', + 63838 => '丹', + 63839 => '寧', + 63840 => '怒', + 63841 => '率', + 63842 => '異', + 63843 => '北', + 63844 => '磻', + 63845 => '便', + 63846 => '復', + 63847 => '不', + 63848 => '泌', + 63849 => '數', + 63850 => '索', + 63851 => '參', + 63852 => '塞', + 63853 => '省', + 63854 => '葉', + 63855 => '說', + 63856 => '殺', + 63857 => '辰', + 63858 => '沈', + 63859 => '拾', + 63860 => '若', + 63861 => '掠', + 63862 => '略', + 63863 => '亮', + 63864 => '兩', + 63865 => '凉', + 63866 => '梁', + 63867 => '糧', + 63868 => '良', + 63869 => '諒', + 63870 => '量', + 63871 => '勵', + 63872 => '呂', + 63873 => '女', + 63874 => '廬', + 63875 => '旅', + 63876 => '濾', + 63877 => '礪', + 63878 => '閭', + 63879 => '驪', + 63880 => '麗', + 63881 => '黎', + 63882 => '力', + 63883 => '曆', + 63884 => '歷', + 63885 => '轢', + 63886 => '年', + 63887 => '憐', + 63888 => '戀', + 63889 => '撚', + 63890 => '漣', + 63891 => '煉', + 63892 => '璉', + 63893 => '秊', + 63894 => '練', + 63895 => '聯', + 63896 => '輦', + 63897 => '蓮', + 63898 => '連', + 63899 => '鍊', + 63900 => '列', + 63901 => '劣', + 63902 => '咽', + 63903 => '烈', + 63904 => '裂', + 63905 => '說', + 63906 => '廉', + 63907 => '念', + 63908 => '捻', + 63909 => '殮', + 63910 => '簾', + 63911 => '獵', + 63912 => '令', + 63913 => '囹', + 63914 => '寧', + 63915 => '嶺', + 63916 => '怜', + 63917 => '玲', + 63918 => '瑩', + 63919 => '羚', + 63920 => '聆', + 63921 => '鈴', + 63922 => '零', + 63923 => '靈', + 63924 => '領', + 63925 => '例', + 63926 => '禮', + 63927 => '醴', + 63928 => '隸', + 63929 => '惡', + 63930 => '了', + 63931 => '僚', + 63932 => '寮', + 63933 => '尿', + 63934 => '料', + 63935 => '樂', + 63936 => '燎', + 63937 => '療', + 63938 => '蓼', + 63939 => '遼', + 63940 => '龍', + 63941 => '暈', + 63942 => '阮', + 63943 => '劉', + 63944 => '杻', + 63945 => '柳', + 63946 => '流', + 63947 => '溜', + 63948 => '琉', + 63949 => '留', + 63950 => '硫', + 63951 => '紐', + 63952 => '類', + 63953 => '六', + 63954 => '戮', + 63955 => '陸', + 63956 => '倫', + 63957 => '崙', + 63958 => '淪', + 63959 => '輪', + 63960 => '律', + 63961 => '慄', + 63962 => '栗', + 63963 => '率', + 63964 => '隆', + 63965 => '利', + 63966 => '吏', + 63967 => '履', + 63968 => '易', + 63969 => '李', + 63970 => '梨', + 63971 => '泥', + 63972 => '理', + 63973 => '痢', + 63974 => '罹', + 63975 => '裏', + 63976 => '裡', + 63977 => '里', + 63978 => '離', + 63979 => '匿', + 63980 => '溺', + 63981 => '吝', + 63982 => '燐', + 63983 => '璘', + 63984 => '藺', + 63985 => '隣', + 63986 => '鱗', + 63987 => '麟', + 63988 => '林', + 63989 => '淋', + 63990 => '臨', + 63991 => '立', + 63992 => '笠', + 63993 => '粒', + 63994 => '狀', + 63995 => '炙', + 63996 => '識', + 63997 => '什', + 63998 => '茶', + 63999 => '刺', + 64000 => '切', + 64001 => '度', + 64002 => '拓', + 64003 => '糖', + 64004 => '宅', + 64005 => '洞', + 64006 => '暴', + 64007 => '輻', + 64008 => '行', + 64009 => '降', + 64010 => '見', + 64011 => '廓', + 64012 => '兀', + 64013 => '嗀', + 64016 => '塚', + 64018 => '晴', + 64021 => '凞', + 64022 => '猪', + 64023 => '益', + 64024 => '礼', + 64025 => '神', + 64026 => '祥', + 64027 => '福', + 64028 => '靖', + 64029 => '精', + 64030 => '羽', + 64032 => '蘒', + 64034 => '諸', + 64037 => '逸', + 64038 => '都', + 64042 => '飯', + 64043 => '飼', + 64044 => '館', + 64045 => '鶴', + 64046 => '郞', + 64047 => '隷', + 64048 => '侮', + 64049 => '僧', + 64050 => '免', + 64051 => '勉', + 64052 => '勤', + 64053 => '卑', + 64054 => '喝', + 64055 => '嘆', + 64056 => '器', + 64057 => '塀', + 64058 => '墨', + 64059 => '層', + 64060 => '屮', + 64061 => '悔', + 64062 => '慨', + 64063 => '憎', + 64064 => '懲', + 64065 => '敏', + 64066 => '既', + 64067 => '暑', + 64068 => '梅', + 64069 => '海', + 64070 => '渚', + 64071 => '漢', + 64072 => '煮', + 64073 => '爫', + 64074 => '琢', + 64075 => '碑', + 64076 => '社', + 64077 => '祉', + 64078 => '祈', + 64079 => '祐', + 64080 => '祖', + 64081 => '祝', + 64082 => '禍', + 64083 => '禎', + 64084 => '穀', + 64085 => '突', + 64086 => '節', + 64087 => '練', + 64088 => '縉', + 64089 => '繁', + 64090 => '署', + 64091 => '者', + 64092 => '臭', + 64093 => '艹', + 64094 => '艹', + 64095 => '著', + 64096 => '褐', + 64097 => '視', + 64098 => '謁', + 64099 => '謹', + 64100 => '賓', + 64101 => '贈', + 64102 => '辶', + 64103 => '逸', + 64104 => '難', + 64105 => '響', + 64106 => '頻', + 64107 => '恵', + 64108 => '𤋮', + 64109 => '舘', + 64112 => '並', + 64113 => '况', + 64114 => '全', + 64115 => '侀', + 64116 => '充', + 64117 => '冀', + 64118 => '勇', + 64119 => '勺', + 64120 => '喝', + 64121 => '啕', + 64122 => '喙', + 64123 => '嗢', + 64124 => '塚', + 64125 => '墳', + 64126 => '奄', + 64127 => '奔', + 64128 => '婢', + 64129 => '嬨', + 64130 => '廒', + 64131 => '廙', + 64132 => '彩', + 64133 => '徭', + 64134 => '惘', + 64135 => '慎', + 64136 => '愈', + 64137 => '憎', + 64138 => '慠', + 64139 => '懲', + 64140 => '戴', + 64141 => '揄', + 64142 => '搜', + 64143 => '摒', + 64144 => '敖', + 64145 => '晴', + 64146 => '朗', + 64147 => '望', + 64148 => '杖', + 64149 => '歹', + 64150 => '殺', + 64151 => '流', + 64152 => '滛', + 64153 => '滋', + 64154 => '漢', + 64155 => '瀞', + 64156 => '煮', + 64157 => '瞧', + 64158 => '爵', + 64159 => '犯', + 64160 => '猪', + 64161 => '瑱', + 64162 => '甆', + 64163 => '画', + 64164 => '瘝', + 64165 => '瘟', + 64166 => '益', + 64167 => '盛', + 64168 => '直', + 64169 => '睊', + 64170 => '着', + 64171 => '磌', + 64172 => '窱', + 64173 => '節', + 64174 => '类', + 64175 => '絛', + 64176 => '練', + 64177 => '缾', + 64178 => '者', + 64179 => '荒', + 64180 => '華', + 64181 => '蝹', + 64182 => '襁', + 64183 => '覆', + 64184 => '視', + 64185 => '調', + 64186 => '諸', + 64187 => '請', + 64188 => '謁', + 64189 => '諾', + 64190 => '諭', + 64191 => '謹', + 64192 => '變', + 64193 => '贈', + 64194 => '輸', + 64195 => '遲', + 64196 => '醙', + 64197 => '鉶', + 64198 => '陼', + 64199 => '難', + 64200 => '靖', + 64201 => '韛', + 64202 => '響', + 64203 => '頋', + 64204 => '頻', + 64205 => '鬒', + 64206 => '龜', + 64207 => '𢡊', + 64208 => '𢡄', + 64209 => '𣏕', + 64210 => '㮝', + 64211 => '䀘', + 64212 => '䀹', + 64213 => '𥉉', + 64214 => '𥳐', + 64215 => '𧻓', + 64216 => '齃', + 64217 => '龎', + 64256 => 'ff', + 64257 => 'fi', + 64258 => 'fl', + 64259 => 'ffi', + 64260 => 'ffl', + 64261 => 'st', + 64262 => 'st', + 64275 => 'մն', + 64276 => 'մե', + 64277 => 'մի', + 64278 => 'վն', + 64279 => 'մխ', + 64285 => 'יִ', + 64287 => 'ײַ', + 64288 => 'ע', + 64289 => 'א', + 64290 => 'ד', + 64291 => 'ה', + 64292 => 'כ', + 64293 => 'ל', + 64294 => 'ם', + 64295 => 'ר', + 64296 => 'ת', + 64298 => 'שׁ', + 64299 => 'שׂ', + 64300 => 'שּׁ', + 64301 => 'שּׂ', + 64302 => 'אַ', + 64303 => 'אָ', + 64304 => 'אּ', + 64305 => 'בּ', + 64306 => 'גּ', + 64307 => 'דּ', + 64308 => 'הּ', + 64309 => 'וּ', + 64310 => 'זּ', + 64312 => 'טּ', + 64313 => 'יּ', + 64314 => 'ךּ', + 64315 => 'כּ', + 64316 => 'לּ', + 64318 => 'מּ', + 64320 => 'נּ', + 64321 => 'סּ', + 64323 => 'ףּ', + 64324 => 'פּ', + 64326 => 'צּ', + 64327 => 'קּ', + 64328 => 'רּ', + 64329 => 'שּ', + 64330 => 'תּ', + 64331 => 'וֹ', + 64332 => 'בֿ', + 64333 => 'כֿ', + 64334 => 'פֿ', + 64335 => 'אל', + 64336 => 'ٱ', + 64337 => 'ٱ', + 64338 => 'ٻ', + 64339 => 'ٻ', + 64340 => 'ٻ', + 64341 => 'ٻ', + 64342 => 'پ', + 64343 => 'پ', + 64344 => 'پ', + 64345 => 'پ', + 64346 => 'ڀ', + 64347 => 'ڀ', + 64348 => 'ڀ', + 64349 => 'ڀ', + 64350 => 'ٺ', + 64351 => 'ٺ', + 64352 => 'ٺ', + 64353 => 'ٺ', + 64354 => 'ٿ', + 64355 => 'ٿ', + 64356 => 'ٿ', + 64357 => 'ٿ', + 64358 => 'ٹ', + 64359 => 'ٹ', + 64360 => 'ٹ', + 64361 => 'ٹ', + 64362 => 'ڤ', + 64363 => 'ڤ', + 64364 => 'ڤ', + 64365 => 'ڤ', + 64366 => 'ڦ', + 64367 => 'ڦ', + 64368 => 'ڦ', + 64369 => 'ڦ', + 64370 => 'ڄ', + 64371 => 'ڄ', + 64372 => 'ڄ', + 64373 => 'ڄ', + 64374 => 'ڃ', + 64375 => 'ڃ', + 64376 => 'ڃ', + 64377 => 'ڃ', + 64378 => 'چ', + 64379 => 'چ', + 64380 => 'چ', + 64381 => 'چ', + 64382 => 'ڇ', + 64383 => 'ڇ', + 64384 => 'ڇ', + 64385 => 'ڇ', + 64386 => 'ڍ', + 64387 => 'ڍ', + 64388 => 'ڌ', + 64389 => 'ڌ', + 64390 => 'ڎ', + 64391 => 'ڎ', + 64392 => 'ڈ', + 64393 => 'ڈ', + 64394 => 'ژ', + 64395 => 'ژ', + 64396 => 'ڑ', + 64397 => 'ڑ', + 64398 => 'ک', + 64399 => 'ک', + 64400 => 'ک', + 64401 => 'ک', + 64402 => 'گ', + 64403 => 'گ', + 64404 => 'گ', + 64405 => 'گ', + 64406 => 'ڳ', + 64407 => 'ڳ', + 64408 => 'ڳ', + 64409 => 'ڳ', + 64410 => 'ڱ', + 64411 => 'ڱ', + 64412 => 'ڱ', + 64413 => 'ڱ', + 64414 => 'ں', + 64415 => 'ں', + 64416 => 'ڻ', + 64417 => 'ڻ', + 64418 => 'ڻ', + 64419 => 'ڻ', + 64420 => 'ۀ', + 64421 => 'ۀ', + 64422 => 'ہ', + 64423 => 'ہ', + 64424 => 'ہ', + 64425 => 'ہ', + 64426 => 'ھ', + 64427 => 'ھ', + 64428 => 'ھ', + 64429 => 'ھ', + 64430 => 'ے', + 64431 => 'ے', + 64432 => 'ۓ', + 64433 => 'ۓ', + 64467 => 'ڭ', + 64468 => 'ڭ', + 64469 => 'ڭ', + 64470 => 'ڭ', + 64471 => 'ۇ', + 64472 => 'ۇ', + 64473 => 'ۆ', + 64474 => 'ۆ', + 64475 => 'ۈ', + 64476 => 'ۈ', + 64477 => 'ۇٴ', + 64478 => 'ۋ', + 64479 => 'ۋ', + 64480 => 'ۅ', + 64481 => 'ۅ', + 64482 => 'ۉ', + 64483 => 'ۉ', + 64484 => 'ې', + 64485 => 'ې', + 64486 => 'ې', + 64487 => 'ې', + 64488 => 'ى', + 64489 => 'ى', + 64490 => 'ئا', + 64491 => 'ئا', + 64492 => 'ئە', + 64493 => 'ئە', + 64494 => 'ئو', + 64495 => 'ئو', + 64496 => 'ئۇ', + 64497 => 'ئۇ', + 64498 => 'ئۆ', + 64499 => 'ئۆ', + 64500 => 'ئۈ', + 64501 => 'ئۈ', + 64502 => 'ئې', + 64503 => 'ئې', + 64504 => 'ئې', + 64505 => 'ئى', + 64506 => 'ئى', + 64507 => 'ئى', + 64508 => 'ی', + 64509 => 'ی', + 64510 => 'ی', + 64511 => 'ی', + 64512 => 'ئج', + 64513 => 'ئح', + 64514 => 'ئم', + 64515 => 'ئى', + 64516 => 'ئي', + 64517 => 'بج', + 64518 => 'بح', + 64519 => 'بخ', + 64520 => 'بم', + 64521 => 'بى', + 64522 => 'بي', + 64523 => 'تج', + 64524 => 'تح', + 64525 => 'تخ', + 64526 => 'تم', + 64527 => 'تى', + 64528 => 'تي', + 64529 => 'ثج', + 64530 => 'ثم', + 64531 => 'ثى', + 64532 => 'ثي', + 64533 => 'جح', + 64534 => 'جم', + 64535 => 'حج', + 64536 => 'حم', + 64537 => 'خج', + 64538 => 'خح', + 64539 => 'خم', + 64540 => 'سج', + 64541 => 'سح', + 64542 => 'سخ', + 64543 => 'سم', + 64544 => 'صح', + 64545 => 'صم', + 64546 => 'ضج', + 64547 => 'ضح', + 64548 => 'ضخ', + 64549 => 'ضم', + 64550 => 'طح', + 64551 => 'طم', + 64552 => 'ظم', + 64553 => 'عج', + 64554 => 'عم', + 64555 => 'غج', + 64556 => 'غم', + 64557 => 'فج', + 64558 => 'فح', + 64559 => 'فخ', + 64560 => 'فم', + 64561 => 'فى', + 64562 => 'في', + 64563 => 'قح', + 64564 => 'قم', + 64565 => 'قى', + 64566 => 'قي', + 64567 => 'كا', + 64568 => 'كج', + 64569 => 'كح', + 64570 => 'كخ', + 64571 => 'كل', + 64572 => 'كم', + 64573 => 'كى', + 64574 => 'كي', + 64575 => 'لج', + 64576 => 'لح', + 64577 => 'لخ', + 64578 => 'لم', + 64579 => 'لى', + 64580 => 'لي', + 64581 => 'مج', + 64582 => 'مح', + 64583 => 'مخ', + 64584 => 'مم', + 64585 => 'مى', + 64586 => 'مي', + 64587 => 'نج', + 64588 => 'نح', + 64589 => 'نخ', + 64590 => 'نم', + 64591 => 'نى', + 64592 => 'ني', + 64593 => 'هج', + 64594 => 'هم', + 64595 => 'هى', + 64596 => 'هي', + 64597 => 'يج', + 64598 => 'يح', + 64599 => 'يخ', + 64600 => 'يم', + 64601 => 'يى', + 64602 => 'يي', + 64603 => 'ذٰ', + 64604 => 'رٰ', + 64605 => 'ىٰ', + 64612 => 'ئر', + 64613 => 'ئز', + 64614 => 'ئم', + 64615 => 'ئن', + 64616 => 'ئى', + 64617 => 'ئي', + 64618 => 'بر', + 64619 => 'بز', + 64620 => 'بم', + 64621 => 'بن', + 64622 => 'بى', + 64623 => 'بي', + 64624 => 'تر', + 64625 => 'تز', + 64626 => 'تم', + 64627 => 'تن', + 64628 => 'تى', + 64629 => 'تي', + 64630 => 'ثر', + 64631 => 'ثز', + 64632 => 'ثم', + 64633 => 'ثن', + 64634 => 'ثى', + 64635 => 'ثي', + 64636 => 'فى', + 64637 => 'في', + 64638 => 'قى', + 64639 => 'قي', + 64640 => 'كا', + 64641 => 'كل', + 64642 => 'كم', + 64643 => 'كى', + 64644 => 'كي', + 64645 => 'لم', + 64646 => 'لى', + 64647 => 'لي', + 64648 => 'ما', + 64649 => 'مم', + 64650 => 'نر', + 64651 => 'نز', + 64652 => 'نم', + 64653 => 'نن', + 64654 => 'نى', + 64655 => 'ني', + 64656 => 'ىٰ', + 64657 => 'ير', + 64658 => 'يز', + 64659 => 'يم', + 64660 => 'ين', + 64661 => 'يى', + 64662 => 'يي', + 64663 => 'ئج', + 64664 => 'ئح', + 64665 => 'ئخ', + 64666 => 'ئم', + 64667 => 'ئه', + 64668 => 'بج', + 64669 => 'بح', + 64670 => 'بخ', + 64671 => 'بم', + 64672 => 'به', + 64673 => 'تج', + 64674 => 'تح', + 64675 => 'تخ', + 64676 => 'تم', + 64677 => 'ته', + 64678 => 'ثم', + 64679 => 'جح', + 64680 => 'جم', + 64681 => 'حج', + 64682 => 'حم', + 64683 => 'خج', + 64684 => 'خم', + 64685 => 'سج', + 64686 => 'سح', + 64687 => 'سخ', + 64688 => 'سم', + 64689 => 'صح', + 64690 => 'صخ', + 64691 => 'صم', + 64692 => 'ضج', + 64693 => 'ضح', + 64694 => 'ضخ', + 64695 => 'ضم', + 64696 => 'طح', + 64697 => 'ظم', + 64698 => 'عج', + 64699 => 'عم', + 64700 => 'غج', + 64701 => 'غم', + 64702 => 'فج', + 64703 => 'فح', + 64704 => 'فخ', + 64705 => 'فم', + 64706 => 'قح', + 64707 => 'قم', + 64708 => 'كج', + 64709 => 'كح', + 64710 => 'كخ', + 64711 => 'كل', + 64712 => 'كم', + 64713 => 'لج', + 64714 => 'لح', + 64715 => 'لخ', + 64716 => 'لم', + 64717 => 'له', + 64718 => 'مج', + 64719 => 'مح', + 64720 => 'مخ', + 64721 => 'مم', + 64722 => 'نج', + 64723 => 'نح', + 64724 => 'نخ', + 64725 => 'نم', + 64726 => 'نه', + 64727 => 'هج', + 64728 => 'هم', + 64729 => 'هٰ', + 64730 => 'يج', + 64731 => 'يح', + 64732 => 'يخ', + 64733 => 'يم', + 64734 => 'يه', + 64735 => 'ئم', + 64736 => 'ئه', + 64737 => 'بم', + 64738 => 'به', + 64739 => 'تم', + 64740 => 'ته', + 64741 => 'ثم', + 64742 => 'ثه', + 64743 => 'سم', + 64744 => 'سه', + 64745 => 'شم', + 64746 => 'شه', + 64747 => 'كل', + 64748 => 'كم', + 64749 => 'لم', + 64750 => 'نم', + 64751 => 'نه', + 64752 => 'يم', + 64753 => 'يه', + 64754 => 'ـَّ', + 64755 => 'ـُّ', + 64756 => 'ـِّ', + 64757 => 'طى', + 64758 => 'طي', + 64759 => 'عى', + 64760 => 'عي', + 64761 => 'غى', + 64762 => 'غي', + 64763 => 'سى', + 64764 => 'سي', + 64765 => 'شى', + 64766 => 'شي', + 64767 => 'حى', + 64768 => 'حي', + 64769 => 'جى', + 64770 => 'جي', + 64771 => 'خى', + 64772 => 'خي', + 64773 => 'صى', + 64774 => 'صي', + 64775 => 'ضى', + 64776 => 'ضي', + 64777 => 'شج', + 64778 => 'شح', + 64779 => 'شخ', + 64780 => 'شم', + 64781 => 'شر', + 64782 => 'سر', + 64783 => 'صر', + 64784 => 'ضر', + 64785 => 'طى', + 64786 => 'طي', + 64787 => 'عى', + 64788 => 'عي', + 64789 => 'غى', + 64790 => 'غي', + 64791 => 'سى', + 64792 => 'سي', + 64793 => 'شى', + 64794 => 'شي', + 64795 => 'حى', + 64796 => 'حي', + 64797 => 'جى', + 64798 => 'جي', + 64799 => 'خى', + 64800 => 'خي', + 64801 => 'صى', + 64802 => 'صي', + 64803 => 'ضى', + 64804 => 'ضي', + 64805 => 'شج', + 64806 => 'شح', + 64807 => 'شخ', + 64808 => 'شم', + 64809 => 'شر', + 64810 => 'سر', + 64811 => 'صر', + 64812 => 'ضر', + 64813 => 'شج', + 64814 => 'شح', + 64815 => 'شخ', + 64816 => 'شم', + 64817 => 'سه', + 64818 => 'شه', + 64819 => 'طم', + 64820 => 'سج', + 64821 => 'سح', + 64822 => 'سخ', + 64823 => 'شج', + 64824 => 'شح', + 64825 => 'شخ', + 64826 => 'طم', + 64827 => 'ظم', + 64828 => 'اً', + 64829 => 'اً', + 64848 => 'تجم', + 64849 => 'تحج', + 64850 => 'تحج', + 64851 => 'تحم', + 64852 => 'تخم', + 64853 => 'تمج', + 64854 => 'تمح', + 64855 => 'تمخ', + 64856 => 'جمح', + 64857 => 'جمح', + 64858 => 'حمي', + 64859 => 'حمى', + 64860 => 'سحج', + 64861 => 'سجح', + 64862 => 'سجى', + 64863 => 'سمح', + 64864 => 'سمح', + 64865 => 'سمج', + 64866 => 'سمم', + 64867 => 'سمم', + 64868 => 'صحح', + 64869 => 'صحح', + 64870 => 'صمم', + 64871 => 'شحم', + 64872 => 'شحم', + 64873 => 'شجي', + 64874 => 'شمخ', + 64875 => 'شمخ', + 64876 => 'شمم', + 64877 => 'شمم', + 64878 => 'ضحى', + 64879 => 'ضخم', + 64880 => 'ضخم', + 64881 => 'طمح', + 64882 => 'طمح', + 64883 => 'طمم', + 64884 => 'طمي', + 64885 => 'عجم', + 64886 => 'عمم', + 64887 => 'عمم', + 64888 => 'عمى', + 64889 => 'غمم', + 64890 => 'غمي', + 64891 => 'غمى', + 64892 => 'فخم', + 64893 => 'فخم', + 64894 => 'قمح', + 64895 => 'قمم', + 64896 => 'لحم', + 64897 => 'لحي', + 64898 => 'لحى', + 64899 => 'لجج', + 64900 => 'لجج', + 64901 => 'لخم', + 64902 => 'لخم', + 64903 => 'لمح', + 64904 => 'لمح', + 64905 => 'محج', + 64906 => 'محم', + 64907 => 'محي', + 64908 => 'مجح', + 64909 => 'مجم', + 64910 => 'مخج', + 64911 => 'مخم', + 64914 => 'مجخ', + 64915 => 'همج', + 64916 => 'همم', + 64917 => 'نحم', + 64918 => 'نحى', + 64919 => 'نجم', + 64920 => 'نجم', + 64921 => 'نجى', + 64922 => 'نمي', + 64923 => 'نمى', + 64924 => 'يمم', + 64925 => 'يمم', + 64926 => 'بخي', + 64927 => 'تجي', + 64928 => 'تجى', + 64929 => 'تخي', + 64930 => 'تخى', + 64931 => 'تمي', + 64932 => 'تمى', + 64933 => 'جمي', + 64934 => 'جحى', + 64935 => 'جمى', + 64936 => 'سخى', + 64937 => 'صحي', + 64938 => 'شحي', + 64939 => 'ضحي', + 64940 => 'لجي', + 64941 => 'لمي', + 64942 => 'يحي', + 64943 => 'يجي', + 64944 => 'يمي', + 64945 => 'ممي', + 64946 => 'قمي', + 64947 => 'نحي', + 64948 => 'قمح', + 64949 => 'لحم', + 64950 => 'عمي', + 64951 => 'كمي', + 64952 => 'نجح', + 64953 => 'مخي', + 64954 => 'لجم', + 64955 => 'كمم', + 64956 => 'لجم', + 64957 => 'نجح', + 64958 => 'جحي', + 64959 => 'حجي', + 64960 => 'مجي', + 64961 => 'فمي', + 64962 => 'بحي', + 64963 => 'كمم', + 64964 => 'عجم', + 64965 => 'صمم', + 64966 => 'سخي', + 64967 => 'نجي', + 65008 => 'صلے', + 65009 => 'قلے', + 65010 => 'الله', + 65011 => 'اكبر', + 65012 => 'محمد', + 65013 => 'صلعم', + 65014 => 'رسول', + 65015 => 'عليه', + 65016 => 'وسلم', + 65017 => 'صلى', + 65020 => 'ریال', + 65041 => '、', + 65047 => '〖', + 65048 => '〗', + 65073 => '—', + 65074 => '–', + 65081 => '〔', + 65082 => '〕', + 65083 => '【', + 65084 => '】', + 65085 => '《', + 65086 => '》', + 65087 => '〈', + 65088 => '〉', + 65089 => '「', + 65090 => '」', + 65091 => '『', + 65092 => '』', + 65105 => '、', + 65112 => '—', + 65117 => '〔', + 65118 => '〕', + 65123 => '-', + 65137 => 'ـً', + 65143 => 'ـَ', + 65145 => 'ـُ', + 65147 => 'ـِ', + 65149 => 'ـّ', + 65151 => 'ـْ', + 65152 => 'ء', + 65153 => 'آ', + 65154 => 'آ', + 65155 => 'أ', + 65156 => 'أ', + 65157 => 'ؤ', + 65158 => 'ؤ', + 65159 => 'إ', + 65160 => 'إ', + 65161 => 'ئ', + 65162 => 'ئ', + 65163 => 'ئ', + 65164 => 'ئ', + 65165 => 'ا', + 65166 => 'ا', + 65167 => 'ب', + 65168 => 'ب', + 65169 => 'ب', + 65170 => 'ب', + 65171 => 'ة', + 65172 => 'ة', + 65173 => 'ت', + 65174 => 'ت', + 65175 => 'ت', + 65176 => 'ت', + 65177 => 'ث', + 65178 => 'ث', + 65179 => 'ث', + 65180 => 'ث', + 65181 => 'ج', + 65182 => 'ج', + 65183 => 'ج', + 65184 => 'ج', + 65185 => 'ح', + 65186 => 'ح', + 65187 => 'ح', + 65188 => 'ح', + 65189 => 'خ', + 65190 => 'خ', + 65191 => 'خ', + 65192 => 'خ', + 65193 => 'د', + 65194 => 'د', + 65195 => 'ذ', + 65196 => 'ذ', + 65197 => 'ر', + 65198 => 'ر', + 65199 => 'ز', + 65200 => 'ز', + 65201 => 'س', + 65202 => 'س', + 65203 => 'س', + 65204 => 'س', + 65205 => 'ش', + 65206 => 'ش', + 65207 => 'ش', + 65208 => 'ش', + 65209 => 'ص', + 65210 => 'ص', + 65211 => 'ص', + 65212 => 'ص', + 65213 => 'ض', + 65214 => 'ض', + 65215 => 'ض', + 65216 => 'ض', + 65217 => 'ط', + 65218 => 'ط', + 65219 => 'ط', + 65220 => 'ط', + 65221 => 'ظ', + 65222 => 'ظ', + 65223 => 'ظ', + 65224 => 'ظ', + 65225 => 'ع', + 65226 => 'ع', + 65227 => 'ع', + 65228 => 'ع', + 65229 => 'غ', + 65230 => 'غ', + 65231 => 'غ', + 65232 => 'غ', + 65233 => 'ف', + 65234 => 'ف', + 65235 => 'ف', + 65236 => 'ف', + 65237 => 'ق', + 65238 => 'ق', + 65239 => 'ق', + 65240 => 'ق', + 65241 => 'ك', + 65242 => 'ك', + 65243 => 'ك', + 65244 => 'ك', + 65245 => 'ل', + 65246 => 'ل', + 65247 => 'ل', + 65248 => 'ل', + 65249 => 'م', + 65250 => 'م', + 65251 => 'م', + 65252 => 'م', + 65253 => 'ن', + 65254 => 'ن', + 65255 => 'ن', + 65256 => 'ن', + 65257 => 'ه', + 65258 => 'ه', + 65259 => 'ه', + 65260 => 'ه', + 65261 => 'و', + 65262 => 'و', + 65263 => 'ى', + 65264 => 'ى', + 65265 => 'ي', + 65266 => 'ي', + 65267 => 'ي', + 65268 => 'ي', + 65269 => 'لآ', + 65270 => 'لآ', + 65271 => 'لأ', + 65272 => 'لأ', + 65273 => 'لإ', + 65274 => 'لإ', + 65275 => 'لا', + 65276 => 'لا', + 65293 => '-', + 65294 => '.', + 65296 => '0', + 65297 => '1', + 65298 => '2', + 65299 => '3', + 65300 => '4', + 65301 => '5', + 65302 => '6', + 65303 => '7', + 65304 => '8', + 65305 => '9', + 65313 => 'a', + 65314 => 'b', + 65315 => 'c', + 65316 => 'd', + 65317 => 'e', + 65318 => 'f', + 65319 => 'g', + 65320 => 'h', + 65321 => 'i', + 65322 => 'j', + 65323 => 'k', + 65324 => 'l', + 65325 => 'm', + 65326 => 'n', + 65327 => 'o', + 65328 => 'p', + 65329 => 'q', + 65330 => 'r', + 65331 => 's', + 65332 => 't', + 65333 => 'u', + 65334 => 'v', + 65335 => 'w', + 65336 => 'x', + 65337 => 'y', + 65338 => 'z', + 65345 => 'a', + 65346 => 'b', + 65347 => 'c', + 65348 => 'd', + 65349 => 'e', + 65350 => 'f', + 65351 => 'g', + 65352 => 'h', + 65353 => 'i', + 65354 => 'j', + 65355 => 'k', + 65356 => 'l', + 65357 => 'm', + 65358 => 'n', + 65359 => 'o', + 65360 => 'p', + 65361 => 'q', + 65362 => 'r', + 65363 => 's', + 65364 => 't', + 65365 => 'u', + 65366 => 'v', + 65367 => 'w', + 65368 => 'x', + 65369 => 'y', + 65370 => 'z', + 65375 => '⦅', + 65376 => '⦆', + 65377 => '.', + 65378 => '「', + 65379 => '」', + 65380 => '、', + 65381 => '・', + 65382 => 'ヲ', + 65383 => 'ァ', + 65384 => 'ィ', + 65385 => 'ゥ', + 65386 => 'ェ', + 65387 => 'ォ', + 65388 => 'ャ', + 65389 => 'ュ', + 65390 => 'ョ', + 65391 => 'ッ', + 65392 => 'ー', + 65393 => 'ア', + 65394 => 'イ', + 65395 => 'ウ', + 65396 => 'エ', + 65397 => 'オ', + 65398 => 'カ', + 65399 => 'キ', + 65400 => 'ク', + 65401 => 'ケ', + 65402 => 'コ', + 65403 => 'サ', + 65404 => 'シ', + 65405 => 'ス', + 65406 => 'セ', + 65407 => 'ソ', + 65408 => 'タ', + 65409 => 'チ', + 65410 => 'ツ', + 65411 => 'テ', + 65412 => 'ト', + 65413 => 'ナ', + 65414 => 'ニ', + 65415 => 'ヌ', + 65416 => 'ネ', + 65417 => 'ノ', + 65418 => 'ハ', + 65419 => 'ヒ', + 65420 => 'フ', + 65421 => 'ヘ', + 65422 => 'ホ', + 65423 => 'マ', + 65424 => 'ミ', + 65425 => 'ム', + 65426 => 'メ', + 65427 => 'モ', + 65428 => 'ヤ', + 65429 => 'ユ', + 65430 => 'ヨ', + 65431 => 'ラ', + 65432 => 'リ', + 65433 => 'ル', + 65434 => 'レ', + 65435 => 'ロ', + 65436 => 'ワ', + 65437 => 'ン', + 65438 => '゙', + 65439 => '゚', + 65441 => 'ᄀ', + 65442 => 'ᄁ', + 65443 => 'ᆪ', + 65444 => 'ᄂ', + 65445 => 'ᆬ', + 65446 => 'ᆭ', + 65447 => 'ᄃ', + 65448 => 'ᄄ', + 65449 => 'ᄅ', + 65450 => 'ᆰ', + 65451 => 'ᆱ', + 65452 => 'ᆲ', + 65453 => 'ᆳ', + 65454 => 'ᆴ', + 65455 => 'ᆵ', + 65456 => 'ᄚ', + 65457 => 'ᄆ', + 65458 => 'ᄇ', + 65459 => 'ᄈ', + 65460 => 'ᄡ', + 65461 => 'ᄉ', + 65462 => 'ᄊ', + 65463 => 'ᄋ', + 65464 => 'ᄌ', + 65465 => 'ᄍ', + 65466 => 'ᄎ', + 65467 => 'ᄏ', + 65468 => 'ᄐ', + 65469 => 'ᄑ', + 65470 => 'ᄒ', + 65474 => 'ᅡ', + 65475 => 'ᅢ', + 65476 => 'ᅣ', + 65477 => 'ᅤ', + 65478 => 'ᅥ', + 65479 => 'ᅦ', + 65482 => 'ᅧ', + 65483 => 'ᅨ', + 65484 => 'ᅩ', + 65485 => 'ᅪ', + 65486 => 'ᅫ', + 65487 => 'ᅬ', + 65490 => 'ᅭ', + 65491 => 'ᅮ', + 65492 => 'ᅯ', + 65493 => 'ᅰ', + 65494 => 'ᅱ', + 65495 => 'ᅲ', + 65498 => 'ᅳ', + 65499 => 'ᅴ', + 65500 => 'ᅵ', + 65504 => '¢', + 65505 => '£', + 65506 => '¬', + 65508 => '¦', + 65509 => '¥', + 65510 => '₩', + 65512 => '│', + 65513 => '←', + 65514 => '↑', + 65515 => '→', + 65516 => '↓', + 65517 => '■', + 65518 => '○', + 66560 => '𐐨', + 66561 => '𐐩', + 66562 => '𐐪', + 66563 => '𐐫', + 66564 => '𐐬', + 66565 => '𐐭', + 66566 => '𐐮', + 66567 => '𐐯', + 66568 => '𐐰', + 66569 => '𐐱', + 66570 => '𐐲', + 66571 => '𐐳', + 66572 => '𐐴', + 66573 => '𐐵', + 66574 => '𐐶', + 66575 => '𐐷', + 66576 => '𐐸', + 66577 => '𐐹', + 66578 => '𐐺', + 66579 => '𐐻', + 66580 => '𐐼', + 66581 => '𐐽', + 66582 => '𐐾', + 66583 => '𐐿', + 66584 => '𐑀', + 66585 => '𐑁', + 66586 => '𐑂', + 66587 => '𐑃', + 66588 => '𐑄', + 66589 => '𐑅', + 66590 => '𐑆', + 66591 => '𐑇', + 66592 => '𐑈', + 66593 => '𐑉', + 66594 => '𐑊', + 66595 => '𐑋', + 66596 => '𐑌', + 66597 => '𐑍', + 66598 => '𐑎', + 66599 => '𐑏', + 66736 => '𐓘', + 66737 => '𐓙', + 66738 => '𐓚', + 66739 => '𐓛', + 66740 => '𐓜', + 66741 => '𐓝', + 66742 => '𐓞', + 66743 => '𐓟', + 66744 => '𐓠', + 66745 => '𐓡', + 66746 => '𐓢', + 66747 => '𐓣', + 66748 => '𐓤', + 66749 => '𐓥', + 66750 => '𐓦', + 66751 => '𐓧', + 66752 => '𐓨', + 66753 => '𐓩', + 66754 => '𐓪', + 66755 => '𐓫', + 66756 => '𐓬', + 66757 => '𐓭', + 66758 => '𐓮', + 66759 => '𐓯', + 66760 => '𐓰', + 66761 => '𐓱', + 66762 => '𐓲', + 66763 => '𐓳', + 66764 => '𐓴', + 66765 => '𐓵', + 66766 => '𐓶', + 66767 => '𐓷', + 66768 => '𐓸', + 66769 => '𐓹', + 66770 => '𐓺', + 66771 => '𐓻', + 68736 => '𐳀', + 68737 => '𐳁', + 68738 => '𐳂', + 68739 => '𐳃', + 68740 => '𐳄', + 68741 => '𐳅', + 68742 => '𐳆', + 68743 => '𐳇', + 68744 => '𐳈', + 68745 => '𐳉', + 68746 => '𐳊', + 68747 => '𐳋', + 68748 => '𐳌', + 68749 => '𐳍', + 68750 => '𐳎', + 68751 => '𐳏', + 68752 => '𐳐', + 68753 => '𐳑', + 68754 => '𐳒', + 68755 => '𐳓', + 68756 => '𐳔', + 68757 => '𐳕', + 68758 => '𐳖', + 68759 => '𐳗', + 68760 => '𐳘', + 68761 => '𐳙', + 68762 => '𐳚', + 68763 => '𐳛', + 68764 => '𐳜', + 68765 => '𐳝', + 68766 => '𐳞', + 68767 => '𐳟', + 68768 => '𐳠', + 68769 => '𐳡', + 68770 => '𐳢', + 68771 => '𐳣', + 68772 => '𐳤', + 68773 => '𐳥', + 68774 => '𐳦', + 68775 => '𐳧', + 68776 => '𐳨', + 68777 => '𐳩', + 68778 => '𐳪', + 68779 => '𐳫', + 68780 => '𐳬', + 68781 => '𐳭', + 68782 => '𐳮', + 68783 => '𐳯', + 68784 => '𐳰', + 68785 => '𐳱', + 68786 => '𐳲', + 71840 => '𑣀', + 71841 => '𑣁', + 71842 => '𑣂', + 71843 => '𑣃', + 71844 => '𑣄', + 71845 => '𑣅', + 71846 => '𑣆', + 71847 => '𑣇', + 71848 => '𑣈', + 71849 => '𑣉', + 71850 => '𑣊', + 71851 => '𑣋', + 71852 => '𑣌', + 71853 => '𑣍', + 71854 => '𑣎', + 71855 => '𑣏', + 71856 => '𑣐', + 71857 => '𑣑', + 71858 => '𑣒', + 71859 => '𑣓', + 71860 => '𑣔', + 71861 => '𑣕', + 71862 => '𑣖', + 71863 => '𑣗', + 71864 => '𑣘', + 71865 => '𑣙', + 71866 => '𑣚', + 71867 => '𑣛', + 71868 => '𑣜', + 71869 => '𑣝', + 71870 => '𑣞', + 71871 => '𑣟', + 93760 => '𖹠', + 93761 => '𖹡', + 93762 => '𖹢', + 93763 => '𖹣', + 93764 => '𖹤', + 93765 => '𖹥', + 93766 => '𖹦', + 93767 => '𖹧', + 93768 => '𖹨', + 93769 => '𖹩', + 93770 => '𖹪', + 93771 => '𖹫', + 93772 => '𖹬', + 93773 => '𖹭', + 93774 => '𖹮', + 93775 => '𖹯', + 93776 => '𖹰', + 93777 => '𖹱', + 93778 => '𖹲', + 93779 => '𖹳', + 93780 => '𖹴', + 93781 => '𖹵', + 93782 => '𖹶', + 93783 => '𖹷', + 93784 => '𖹸', + 93785 => '𖹹', + 93786 => '𖹺', + 93787 => '𖹻', + 93788 => '𖹼', + 93789 => '𖹽', + 93790 => '𖹾', + 93791 => '𖹿', + 119134 => '𝅗𝅥', + 119135 => '𝅘𝅥', + 119136 => '𝅘𝅥𝅮', + 119137 => '𝅘𝅥𝅯', + 119138 => '𝅘𝅥𝅰', + 119139 => '𝅘𝅥𝅱', + 119140 => '𝅘𝅥𝅲', + 119227 => '𝆹𝅥', + 119228 => '𝆺𝅥', + 119229 => '𝆹𝅥𝅮', + 119230 => '𝆺𝅥𝅮', + 119231 => '𝆹𝅥𝅯', + 119232 => '𝆺𝅥𝅯', + 119808 => 'a', + 119809 => 'b', + 119810 => 'c', + 119811 => 'd', + 119812 => 'e', + 119813 => 'f', + 119814 => 'g', + 119815 => 'h', + 119816 => 'i', + 119817 => 'j', + 119818 => 'k', + 119819 => 'l', + 119820 => 'm', + 119821 => 'n', + 119822 => 'o', + 119823 => 'p', + 119824 => 'q', + 119825 => 'r', + 119826 => 's', + 119827 => 't', + 119828 => 'u', + 119829 => 'v', + 119830 => 'w', + 119831 => 'x', + 119832 => 'y', + 119833 => 'z', + 119834 => 'a', + 119835 => 'b', + 119836 => 'c', + 119837 => 'd', + 119838 => 'e', + 119839 => 'f', + 119840 => 'g', + 119841 => 'h', + 119842 => 'i', + 119843 => 'j', + 119844 => 'k', + 119845 => 'l', + 119846 => 'm', + 119847 => 'n', + 119848 => 'o', + 119849 => 'p', + 119850 => 'q', + 119851 => 'r', + 119852 => 's', + 119853 => 't', + 119854 => 'u', + 119855 => 'v', + 119856 => 'w', + 119857 => 'x', + 119858 => 'y', + 119859 => 'z', + 119860 => 'a', + 119861 => 'b', + 119862 => 'c', + 119863 => 'd', + 119864 => 'e', + 119865 => 'f', + 119866 => 'g', + 119867 => 'h', + 119868 => 'i', + 119869 => 'j', + 119870 => 'k', + 119871 => 'l', + 119872 => 'm', + 119873 => 'n', + 119874 => 'o', + 119875 => 'p', + 119876 => 'q', + 119877 => 'r', + 119878 => 's', + 119879 => 't', + 119880 => 'u', + 119881 => 'v', + 119882 => 'w', + 119883 => 'x', + 119884 => 'y', + 119885 => 'z', + 119886 => 'a', + 119887 => 'b', + 119888 => 'c', + 119889 => 'd', + 119890 => 'e', + 119891 => 'f', + 119892 => 'g', + 119894 => 'i', + 119895 => 'j', + 119896 => 'k', + 119897 => 'l', + 119898 => 'm', + 119899 => 'n', + 119900 => 'o', + 119901 => 'p', + 119902 => 'q', + 119903 => 'r', + 119904 => 's', + 119905 => 't', + 119906 => 'u', + 119907 => 'v', + 119908 => 'w', + 119909 => 'x', + 119910 => 'y', + 119911 => 'z', + 119912 => 'a', + 119913 => 'b', + 119914 => 'c', + 119915 => 'd', + 119916 => 'e', + 119917 => 'f', + 119918 => 'g', + 119919 => 'h', + 119920 => 'i', + 119921 => 'j', + 119922 => 'k', + 119923 => 'l', + 119924 => 'm', + 119925 => 'n', + 119926 => 'o', + 119927 => 'p', + 119928 => 'q', + 119929 => 'r', + 119930 => 's', + 119931 => 't', + 119932 => 'u', + 119933 => 'v', + 119934 => 'w', + 119935 => 'x', + 119936 => 'y', + 119937 => 'z', + 119938 => 'a', + 119939 => 'b', + 119940 => 'c', + 119941 => 'd', + 119942 => 'e', + 119943 => 'f', + 119944 => 'g', + 119945 => 'h', + 119946 => 'i', + 119947 => 'j', + 119948 => 'k', + 119949 => 'l', + 119950 => 'm', + 119951 => 'n', + 119952 => 'o', + 119953 => 'p', + 119954 => 'q', + 119955 => 'r', + 119956 => 's', + 119957 => 't', + 119958 => 'u', + 119959 => 'v', + 119960 => 'w', + 119961 => 'x', + 119962 => 'y', + 119963 => 'z', + 119964 => 'a', + 119966 => 'c', + 119967 => 'd', + 119970 => 'g', + 119973 => 'j', + 119974 => 'k', + 119977 => 'n', + 119978 => 'o', + 119979 => 'p', + 119980 => 'q', + 119982 => 's', + 119983 => 't', + 119984 => 'u', + 119985 => 'v', + 119986 => 'w', + 119987 => 'x', + 119988 => 'y', + 119989 => 'z', + 119990 => 'a', + 119991 => 'b', + 119992 => 'c', + 119993 => 'd', + 119995 => 'f', + 119997 => 'h', + 119998 => 'i', + 119999 => 'j', + 120000 => 'k', + 120001 => 'l', + 120002 => 'm', + 120003 => 'n', + 120005 => 'p', + 120006 => 'q', + 120007 => 'r', + 120008 => 's', + 120009 => 't', + 120010 => 'u', + 120011 => 'v', + 120012 => 'w', + 120013 => 'x', + 120014 => 'y', + 120015 => 'z', + 120016 => 'a', + 120017 => 'b', + 120018 => 'c', + 120019 => 'd', + 120020 => 'e', + 120021 => 'f', + 120022 => 'g', + 120023 => 'h', + 120024 => 'i', + 120025 => 'j', + 120026 => 'k', + 120027 => 'l', + 120028 => 'm', + 120029 => 'n', + 120030 => 'o', + 120031 => 'p', + 120032 => 'q', + 120033 => 'r', + 120034 => 's', + 120035 => 't', + 120036 => 'u', + 120037 => 'v', + 120038 => 'w', + 120039 => 'x', + 120040 => 'y', + 120041 => 'z', + 120042 => 'a', + 120043 => 'b', + 120044 => 'c', + 120045 => 'd', + 120046 => 'e', + 120047 => 'f', + 120048 => 'g', + 120049 => 'h', + 120050 => 'i', + 120051 => 'j', + 120052 => 'k', + 120053 => 'l', + 120054 => 'm', + 120055 => 'n', + 120056 => 'o', + 120057 => 'p', + 120058 => 'q', + 120059 => 'r', + 120060 => 's', + 120061 => 't', + 120062 => 'u', + 120063 => 'v', + 120064 => 'w', + 120065 => 'x', + 120066 => 'y', + 120067 => 'z', + 120068 => 'a', + 120069 => 'b', + 120071 => 'd', + 120072 => 'e', + 120073 => 'f', + 120074 => 'g', + 120077 => 'j', + 120078 => 'k', + 120079 => 'l', + 120080 => 'm', + 120081 => 'n', + 120082 => 'o', + 120083 => 'p', + 120084 => 'q', + 120086 => 's', + 120087 => 't', + 120088 => 'u', + 120089 => 'v', + 120090 => 'w', + 120091 => 'x', + 120092 => 'y', + 120094 => 'a', + 120095 => 'b', + 120096 => 'c', + 120097 => 'd', + 120098 => 'e', + 120099 => 'f', + 120100 => 'g', + 120101 => 'h', + 120102 => 'i', + 120103 => 'j', + 120104 => 'k', + 120105 => 'l', + 120106 => 'm', + 120107 => 'n', + 120108 => 'o', + 120109 => 'p', + 120110 => 'q', + 120111 => 'r', + 120112 => 's', + 120113 => 't', + 120114 => 'u', + 120115 => 'v', + 120116 => 'w', + 120117 => 'x', + 120118 => 'y', + 120119 => 'z', + 120120 => 'a', + 120121 => 'b', + 120123 => 'd', + 120124 => 'e', + 120125 => 'f', + 120126 => 'g', + 120128 => 'i', + 120129 => 'j', + 120130 => 'k', + 120131 => 'l', + 120132 => 'm', + 120134 => 'o', + 120138 => 's', + 120139 => 't', + 120140 => 'u', + 120141 => 'v', + 120142 => 'w', + 120143 => 'x', + 120144 => 'y', + 120146 => 'a', + 120147 => 'b', + 120148 => 'c', + 120149 => 'd', + 120150 => 'e', + 120151 => 'f', + 120152 => 'g', + 120153 => 'h', + 120154 => 'i', + 120155 => 'j', + 120156 => 'k', + 120157 => 'l', + 120158 => 'm', + 120159 => 'n', + 120160 => 'o', + 120161 => 'p', + 120162 => 'q', + 120163 => 'r', + 120164 => 's', + 120165 => 't', + 120166 => 'u', + 120167 => 'v', + 120168 => 'w', + 120169 => 'x', + 120170 => 'y', + 120171 => 'z', + 120172 => 'a', + 120173 => 'b', + 120174 => 'c', + 120175 => 'd', + 120176 => 'e', + 120177 => 'f', + 120178 => 'g', + 120179 => 'h', + 120180 => 'i', + 120181 => 'j', + 120182 => 'k', + 120183 => 'l', + 120184 => 'm', + 120185 => 'n', + 120186 => 'o', + 120187 => 'p', + 120188 => 'q', + 120189 => 'r', + 120190 => 's', + 120191 => 't', + 120192 => 'u', + 120193 => 'v', + 120194 => 'w', + 120195 => 'x', + 120196 => 'y', + 120197 => 'z', + 120198 => 'a', + 120199 => 'b', + 120200 => 'c', + 120201 => 'd', + 120202 => 'e', + 120203 => 'f', + 120204 => 'g', + 120205 => 'h', + 120206 => 'i', + 120207 => 'j', + 120208 => 'k', + 120209 => 'l', + 120210 => 'm', + 120211 => 'n', + 120212 => 'o', + 120213 => 'p', + 120214 => 'q', + 120215 => 'r', + 120216 => 's', + 120217 => 't', + 120218 => 'u', + 120219 => 'v', + 120220 => 'w', + 120221 => 'x', + 120222 => 'y', + 120223 => 'z', + 120224 => 'a', + 120225 => 'b', + 120226 => 'c', + 120227 => 'd', + 120228 => 'e', + 120229 => 'f', + 120230 => 'g', + 120231 => 'h', + 120232 => 'i', + 120233 => 'j', + 120234 => 'k', + 120235 => 'l', + 120236 => 'm', + 120237 => 'n', + 120238 => 'o', + 120239 => 'p', + 120240 => 'q', + 120241 => 'r', + 120242 => 's', + 120243 => 't', + 120244 => 'u', + 120245 => 'v', + 120246 => 'w', + 120247 => 'x', + 120248 => 'y', + 120249 => 'z', + 120250 => 'a', + 120251 => 'b', + 120252 => 'c', + 120253 => 'd', + 120254 => 'e', + 120255 => 'f', + 120256 => 'g', + 120257 => 'h', + 120258 => 'i', + 120259 => 'j', + 120260 => 'k', + 120261 => 'l', + 120262 => 'm', + 120263 => 'n', + 120264 => 'o', + 120265 => 'p', + 120266 => 'q', + 120267 => 'r', + 120268 => 's', + 120269 => 't', + 120270 => 'u', + 120271 => 'v', + 120272 => 'w', + 120273 => 'x', + 120274 => 'y', + 120275 => 'z', + 120276 => 'a', + 120277 => 'b', + 120278 => 'c', + 120279 => 'd', + 120280 => 'e', + 120281 => 'f', + 120282 => 'g', + 120283 => 'h', + 120284 => 'i', + 120285 => 'j', + 120286 => 'k', + 120287 => 'l', + 120288 => 'm', + 120289 => 'n', + 120290 => 'o', + 120291 => 'p', + 120292 => 'q', + 120293 => 'r', + 120294 => 's', + 120295 => 't', + 120296 => 'u', + 120297 => 'v', + 120298 => 'w', + 120299 => 'x', + 120300 => 'y', + 120301 => 'z', + 120302 => 'a', + 120303 => 'b', + 120304 => 'c', + 120305 => 'd', + 120306 => 'e', + 120307 => 'f', + 120308 => 'g', + 120309 => 'h', + 120310 => 'i', + 120311 => 'j', + 120312 => 'k', + 120313 => 'l', + 120314 => 'm', + 120315 => 'n', + 120316 => 'o', + 120317 => 'p', + 120318 => 'q', + 120319 => 'r', + 120320 => 's', + 120321 => 't', + 120322 => 'u', + 120323 => 'v', + 120324 => 'w', + 120325 => 'x', + 120326 => 'y', + 120327 => 'z', + 120328 => 'a', + 120329 => 'b', + 120330 => 'c', + 120331 => 'd', + 120332 => 'e', + 120333 => 'f', + 120334 => 'g', + 120335 => 'h', + 120336 => 'i', + 120337 => 'j', + 120338 => 'k', + 120339 => 'l', + 120340 => 'm', + 120341 => 'n', + 120342 => 'o', + 120343 => 'p', + 120344 => 'q', + 120345 => 'r', + 120346 => 's', + 120347 => 't', + 120348 => 'u', + 120349 => 'v', + 120350 => 'w', + 120351 => 'x', + 120352 => 'y', + 120353 => 'z', + 120354 => 'a', + 120355 => 'b', + 120356 => 'c', + 120357 => 'd', + 120358 => 'e', + 120359 => 'f', + 120360 => 'g', + 120361 => 'h', + 120362 => 'i', + 120363 => 'j', + 120364 => 'k', + 120365 => 'l', + 120366 => 'm', + 120367 => 'n', + 120368 => 'o', + 120369 => 'p', + 120370 => 'q', + 120371 => 'r', + 120372 => 's', + 120373 => 't', + 120374 => 'u', + 120375 => 'v', + 120376 => 'w', + 120377 => 'x', + 120378 => 'y', + 120379 => 'z', + 120380 => 'a', + 120381 => 'b', + 120382 => 'c', + 120383 => 'd', + 120384 => 'e', + 120385 => 'f', + 120386 => 'g', + 120387 => 'h', + 120388 => 'i', + 120389 => 'j', + 120390 => 'k', + 120391 => 'l', + 120392 => 'm', + 120393 => 'n', + 120394 => 'o', + 120395 => 'p', + 120396 => 'q', + 120397 => 'r', + 120398 => 's', + 120399 => 't', + 120400 => 'u', + 120401 => 'v', + 120402 => 'w', + 120403 => 'x', + 120404 => 'y', + 120405 => 'z', + 120406 => 'a', + 120407 => 'b', + 120408 => 'c', + 120409 => 'd', + 120410 => 'e', + 120411 => 'f', + 120412 => 'g', + 120413 => 'h', + 120414 => 'i', + 120415 => 'j', + 120416 => 'k', + 120417 => 'l', + 120418 => 'm', + 120419 => 'n', + 120420 => 'o', + 120421 => 'p', + 120422 => 'q', + 120423 => 'r', + 120424 => 's', + 120425 => 't', + 120426 => 'u', + 120427 => 'v', + 120428 => 'w', + 120429 => 'x', + 120430 => 'y', + 120431 => 'z', + 120432 => 'a', + 120433 => 'b', + 120434 => 'c', + 120435 => 'd', + 120436 => 'e', + 120437 => 'f', + 120438 => 'g', + 120439 => 'h', + 120440 => 'i', + 120441 => 'j', + 120442 => 'k', + 120443 => 'l', + 120444 => 'm', + 120445 => 'n', + 120446 => 'o', + 120447 => 'p', + 120448 => 'q', + 120449 => 'r', + 120450 => 's', + 120451 => 't', + 120452 => 'u', + 120453 => 'v', + 120454 => 'w', + 120455 => 'x', + 120456 => 'y', + 120457 => 'z', + 120458 => 'a', + 120459 => 'b', + 120460 => 'c', + 120461 => 'd', + 120462 => 'e', + 120463 => 'f', + 120464 => 'g', + 120465 => 'h', + 120466 => 'i', + 120467 => 'j', + 120468 => 'k', + 120469 => 'l', + 120470 => 'm', + 120471 => 'n', + 120472 => 'o', + 120473 => 'p', + 120474 => 'q', + 120475 => 'r', + 120476 => 's', + 120477 => 't', + 120478 => 'u', + 120479 => 'v', + 120480 => 'w', + 120481 => 'x', + 120482 => 'y', + 120483 => 'z', + 120484 => 'ı', + 120485 => 'ȷ', + 120488 => 'α', + 120489 => 'β', + 120490 => 'γ', + 120491 => 'δ', + 120492 => 'ε', + 120493 => 'ζ', + 120494 => 'η', + 120495 => 'θ', + 120496 => 'ι', + 120497 => 'κ', + 120498 => 'λ', + 120499 => 'μ', + 120500 => 'ν', + 120501 => 'ξ', + 120502 => 'ο', + 120503 => 'π', + 120504 => 'ρ', + 120505 => 'θ', + 120506 => 'σ', + 120507 => 'τ', + 120508 => 'υ', + 120509 => 'φ', + 120510 => 'χ', + 120511 => 'ψ', + 120512 => 'ω', + 120513 => '∇', + 120514 => 'α', + 120515 => 'β', + 120516 => 'γ', + 120517 => 'δ', + 120518 => 'ε', + 120519 => 'ζ', + 120520 => 'η', + 120521 => 'θ', + 120522 => 'ι', + 120523 => 'κ', + 120524 => 'λ', + 120525 => 'μ', + 120526 => 'ν', + 120527 => 'ξ', + 120528 => 'ο', + 120529 => 'π', + 120530 => 'ρ', + 120531 => 'σ', + 120532 => 'σ', + 120533 => 'τ', + 120534 => 'υ', + 120535 => 'φ', + 120536 => 'χ', + 120537 => 'ψ', + 120538 => 'ω', + 120539 => '∂', + 120540 => 'ε', + 120541 => 'θ', + 120542 => 'κ', + 120543 => 'φ', + 120544 => 'ρ', + 120545 => 'π', + 120546 => 'α', + 120547 => 'β', + 120548 => 'γ', + 120549 => 'δ', + 120550 => 'ε', + 120551 => 'ζ', + 120552 => 'η', + 120553 => 'θ', + 120554 => 'ι', + 120555 => 'κ', + 120556 => 'λ', + 120557 => 'μ', + 120558 => 'ν', + 120559 => 'ξ', + 120560 => 'ο', + 120561 => 'π', + 120562 => 'ρ', + 120563 => 'θ', + 120564 => 'σ', + 120565 => 'τ', + 120566 => 'υ', + 120567 => 'φ', + 120568 => 'χ', + 120569 => 'ψ', + 120570 => 'ω', + 120571 => '∇', + 120572 => 'α', + 120573 => 'β', + 120574 => 'γ', + 120575 => 'δ', + 120576 => 'ε', + 120577 => 'ζ', + 120578 => 'η', + 120579 => 'θ', + 120580 => 'ι', + 120581 => 'κ', + 120582 => 'λ', + 120583 => 'μ', + 120584 => 'ν', + 120585 => 'ξ', + 120586 => 'ο', + 120587 => 'π', + 120588 => 'ρ', + 120589 => 'σ', + 120590 => 'σ', + 120591 => 'τ', + 120592 => 'υ', + 120593 => 'φ', + 120594 => 'χ', + 120595 => 'ψ', + 120596 => 'ω', + 120597 => '∂', + 120598 => 'ε', + 120599 => 'θ', + 120600 => 'κ', + 120601 => 'φ', + 120602 => 'ρ', + 120603 => 'π', + 120604 => 'α', + 120605 => 'β', + 120606 => 'γ', + 120607 => 'δ', + 120608 => 'ε', + 120609 => 'ζ', + 120610 => 'η', + 120611 => 'θ', + 120612 => 'ι', + 120613 => 'κ', + 120614 => 'λ', + 120615 => 'μ', + 120616 => 'ν', + 120617 => 'ξ', + 120618 => 'ο', + 120619 => 'π', + 120620 => 'ρ', + 120621 => 'θ', + 120622 => 'σ', + 120623 => 'τ', + 120624 => 'υ', + 120625 => 'φ', + 120626 => 'χ', + 120627 => 'ψ', + 120628 => 'ω', + 120629 => '∇', + 120630 => 'α', + 120631 => 'β', + 120632 => 'γ', + 120633 => 'δ', + 120634 => 'ε', + 120635 => 'ζ', + 120636 => 'η', + 120637 => 'θ', + 120638 => 'ι', + 120639 => 'κ', + 120640 => 'λ', + 120641 => 'μ', + 120642 => 'ν', + 120643 => 'ξ', + 120644 => 'ο', + 120645 => 'π', + 120646 => 'ρ', + 120647 => 'σ', + 120648 => 'σ', + 120649 => 'τ', + 120650 => 'υ', + 120651 => 'φ', + 120652 => 'χ', + 120653 => 'ψ', + 120654 => 'ω', + 120655 => '∂', + 120656 => 'ε', + 120657 => 'θ', + 120658 => 'κ', + 120659 => 'φ', + 120660 => 'ρ', + 120661 => 'π', + 120662 => 'α', + 120663 => 'β', + 120664 => 'γ', + 120665 => 'δ', + 120666 => 'ε', + 120667 => 'ζ', + 120668 => 'η', + 120669 => 'θ', + 120670 => 'ι', + 120671 => 'κ', + 120672 => 'λ', + 120673 => 'μ', + 120674 => 'ν', + 120675 => 'ξ', + 120676 => 'ο', + 120677 => 'π', + 120678 => 'ρ', + 120679 => 'θ', + 120680 => 'σ', + 120681 => 'τ', + 120682 => 'υ', + 120683 => 'φ', + 120684 => 'χ', + 120685 => 'ψ', + 120686 => 'ω', + 120687 => '∇', + 120688 => 'α', + 120689 => 'β', + 120690 => 'γ', + 120691 => 'δ', + 120692 => 'ε', + 120693 => 'ζ', + 120694 => 'η', + 120695 => 'θ', + 120696 => 'ι', + 120697 => 'κ', + 120698 => 'λ', + 120699 => 'μ', + 120700 => 'ν', + 120701 => 'ξ', + 120702 => 'ο', + 120703 => 'π', + 120704 => 'ρ', + 120705 => 'σ', + 120706 => 'σ', + 120707 => 'τ', + 120708 => 'υ', + 120709 => 'φ', + 120710 => 'χ', + 120711 => 'ψ', + 120712 => 'ω', + 120713 => '∂', + 120714 => 'ε', + 120715 => 'θ', + 120716 => 'κ', + 120717 => 'φ', + 120718 => 'ρ', + 120719 => 'π', + 120720 => 'α', + 120721 => 'β', + 120722 => 'γ', + 120723 => 'δ', + 120724 => 'ε', + 120725 => 'ζ', + 120726 => 'η', + 120727 => 'θ', + 120728 => 'ι', + 120729 => 'κ', + 120730 => 'λ', + 120731 => 'μ', + 120732 => 'ν', + 120733 => 'ξ', + 120734 => 'ο', + 120735 => 'π', + 120736 => 'ρ', + 120737 => 'θ', + 120738 => 'σ', + 120739 => 'τ', + 120740 => 'υ', + 120741 => 'φ', + 120742 => 'χ', + 120743 => 'ψ', + 120744 => 'ω', + 120745 => '∇', + 120746 => 'α', + 120747 => 'β', + 120748 => 'γ', + 120749 => 'δ', + 120750 => 'ε', + 120751 => 'ζ', + 120752 => 'η', + 120753 => 'θ', + 120754 => 'ι', + 120755 => 'κ', + 120756 => 'λ', + 120757 => 'μ', + 120758 => 'ν', + 120759 => 'ξ', + 120760 => 'ο', + 120761 => 'π', + 120762 => 'ρ', + 120763 => 'σ', + 120764 => 'σ', + 120765 => 'τ', + 120766 => 'υ', + 120767 => 'φ', + 120768 => 'χ', + 120769 => 'ψ', + 120770 => 'ω', + 120771 => '∂', + 120772 => 'ε', + 120773 => 'θ', + 120774 => 'κ', + 120775 => 'φ', + 120776 => 'ρ', + 120777 => 'π', + 120778 => 'ϝ', + 120779 => 'ϝ', + 120782 => '0', + 120783 => '1', + 120784 => '2', + 120785 => '3', + 120786 => '4', + 120787 => '5', + 120788 => '6', + 120789 => '7', + 120790 => '8', + 120791 => '9', + 120792 => '0', + 120793 => '1', + 120794 => '2', + 120795 => '3', + 120796 => '4', + 120797 => '5', + 120798 => '6', + 120799 => '7', + 120800 => '8', + 120801 => '9', + 120802 => '0', + 120803 => '1', + 120804 => '2', + 120805 => '3', + 120806 => '4', + 120807 => '5', + 120808 => '6', + 120809 => '7', + 120810 => '8', + 120811 => '9', + 120812 => '0', + 120813 => '1', + 120814 => '2', + 120815 => '3', + 120816 => '4', + 120817 => '5', + 120818 => '6', + 120819 => '7', + 120820 => '8', + 120821 => '9', + 120822 => '0', + 120823 => '1', + 120824 => '2', + 120825 => '3', + 120826 => '4', + 120827 => '5', + 120828 => '6', + 120829 => '7', + 120830 => '8', + 120831 => '9', + 125184 => '𞤢', + 125185 => '𞤣', + 125186 => '𞤤', + 125187 => '𞤥', + 125188 => '𞤦', + 125189 => '𞤧', + 125190 => '𞤨', + 125191 => '𞤩', + 125192 => '𞤪', + 125193 => '𞤫', + 125194 => '𞤬', + 125195 => '𞤭', + 125196 => '𞤮', + 125197 => '𞤯', + 125198 => '𞤰', + 125199 => '𞤱', + 125200 => '𞤲', + 125201 => '𞤳', + 125202 => '𞤴', + 125203 => '𞤵', + 125204 => '𞤶', + 125205 => '𞤷', + 125206 => '𞤸', + 125207 => '𞤹', + 125208 => '𞤺', + 125209 => '𞤻', + 125210 => '𞤼', + 125211 => '𞤽', + 125212 => '𞤾', + 125213 => '𞤿', + 125214 => '𞥀', + 125215 => '𞥁', + 125216 => '𞥂', + 125217 => '𞥃', + 126464 => 'ا', + 126465 => 'ب', + 126466 => 'ج', + 126467 => 'د', + 126469 => 'و', + 126470 => 'ز', + 126471 => 'ح', + 126472 => 'ط', + 126473 => 'ي', + 126474 => 'ك', + 126475 => 'ل', + 126476 => 'م', + 126477 => 'ن', + 126478 => 'س', + 126479 => 'ع', + 126480 => 'ف', + 126481 => 'ص', + 126482 => 'ق', + 126483 => 'ر', + 126484 => 'ش', + 126485 => 'ت', + 126486 => 'ث', + 126487 => 'خ', + 126488 => 'ذ', + 126489 => 'ض', + 126490 => 'ظ', + 126491 => 'غ', + 126492 => 'ٮ', + 126493 => 'ں', + 126494 => 'ڡ', + 126495 => 'ٯ', + 126497 => 'ب', + 126498 => 'ج', + 126500 => 'ه', + 126503 => 'ح', + 126505 => 'ي', + 126506 => 'ك', + 126507 => 'ل', + 126508 => 'م', + 126509 => 'ن', + 126510 => 'س', + 126511 => 'ع', + 126512 => 'ف', + 126513 => 'ص', + 126514 => 'ق', + 126516 => 'ش', + 126517 => 'ت', + 126518 => 'ث', + 126519 => 'خ', + 126521 => 'ض', + 126523 => 'غ', + 126530 => 'ج', + 126535 => 'ح', + 126537 => 'ي', + 126539 => 'ل', + 126541 => 'ن', + 126542 => 'س', + 126543 => 'ع', + 126545 => 'ص', + 126546 => 'ق', + 126548 => 'ش', + 126551 => 'خ', + 126553 => 'ض', + 126555 => 'غ', + 126557 => 'ں', + 126559 => 'ٯ', + 126561 => 'ب', + 126562 => 'ج', + 126564 => 'ه', + 126567 => 'ح', + 126568 => 'ط', + 126569 => 'ي', + 126570 => 'ك', + 126572 => 'م', + 126573 => 'ن', + 126574 => 'س', + 126575 => 'ع', + 126576 => 'ف', + 126577 => 'ص', + 126578 => 'ق', + 126580 => 'ش', + 126581 => 'ت', + 126582 => 'ث', + 126583 => 'خ', + 126585 => 'ض', + 126586 => 'ظ', + 126587 => 'غ', + 126588 => 'ٮ', + 126590 => 'ڡ', + 126592 => 'ا', + 126593 => 'ب', + 126594 => 'ج', + 126595 => 'د', + 126596 => 'ه', + 126597 => 'و', + 126598 => 'ز', + 126599 => 'ح', + 126600 => 'ط', + 126601 => 'ي', + 126603 => 'ل', + 126604 => 'م', + 126605 => 'ن', + 126606 => 'س', + 126607 => 'ع', + 126608 => 'ف', + 126609 => 'ص', + 126610 => 'ق', + 126611 => 'ر', + 126612 => 'ش', + 126613 => 'ت', + 126614 => 'ث', + 126615 => 'خ', + 126616 => 'ذ', + 126617 => 'ض', + 126618 => 'ظ', + 126619 => 'غ', + 126625 => 'ب', + 126626 => 'ج', + 126627 => 'د', + 126629 => 'و', + 126630 => 'ز', + 126631 => 'ح', + 126632 => 'ط', + 126633 => 'ي', + 126635 => 'ل', + 126636 => 'م', + 126637 => 'ن', + 126638 => 'س', + 126639 => 'ع', + 126640 => 'ف', + 126641 => 'ص', + 126642 => 'ق', + 126643 => 'ر', + 126644 => 'ش', + 126645 => 'ت', + 126646 => 'ث', + 126647 => 'خ', + 126648 => 'ذ', + 126649 => 'ض', + 126650 => 'ظ', + 126651 => 'غ', + 127274 => '〔s〕', + 127275 => 'c', + 127276 => 'r', + 127277 => 'cd', + 127278 => 'wz', + 127280 => 'a', + 127281 => 'b', + 127282 => 'c', + 127283 => 'd', + 127284 => 'e', + 127285 => 'f', + 127286 => 'g', + 127287 => 'h', + 127288 => 'i', + 127289 => 'j', + 127290 => 'k', + 127291 => 'l', + 127292 => 'm', + 127293 => 'n', + 127294 => 'o', + 127295 => 'p', + 127296 => 'q', + 127297 => 'r', + 127298 => 's', + 127299 => 't', + 127300 => 'u', + 127301 => 'v', + 127302 => 'w', + 127303 => 'x', + 127304 => 'y', + 127305 => 'z', + 127306 => 'hv', + 127307 => 'mv', + 127308 => 'sd', + 127309 => 'ss', + 127310 => 'ppv', + 127311 => 'wc', + 127338 => 'mc', + 127339 => 'md', + 127340 => 'mr', + 127376 => 'dj', + 127488 => 'ほか', + 127489 => 'ココ', + 127490 => 'サ', + 127504 => '手', + 127505 => '字', + 127506 => '双', + 127507 => 'デ', + 127508 => '二', + 127509 => '多', + 127510 => '解', + 127511 => '天', + 127512 => '交', + 127513 => '映', + 127514 => '無', + 127515 => '料', + 127516 => '前', + 127517 => '後', + 127518 => '再', + 127519 => '新', + 127520 => '初', + 127521 => '終', + 127522 => '生', + 127523 => '販', + 127524 => '声', + 127525 => '吹', + 127526 => '演', + 127527 => '投', + 127528 => '捕', + 127529 => '一', + 127530 => '三', + 127531 => '遊', + 127532 => '左', + 127533 => '中', + 127534 => '右', + 127535 => '指', + 127536 => '走', + 127537 => '打', + 127538 => '禁', + 127539 => '空', + 127540 => '合', + 127541 => '満', + 127542 => '有', + 127543 => '月', + 127544 => '申', + 127545 => '割', + 127546 => '営', + 127547 => '配', + 127552 => '〔本〕', + 127553 => '〔三〕', + 127554 => '〔二〕', + 127555 => '〔安〕', + 127556 => '〔点〕', + 127557 => '〔打〕', + 127558 => '〔盗〕', + 127559 => '〔勝〕', + 127560 => '〔敗〕', + 127568 => '得', + 127569 => '可', + 130032 => '0', + 130033 => '1', + 130034 => '2', + 130035 => '3', + 130036 => '4', + 130037 => '5', + 130038 => '6', + 130039 => '7', + 130040 => '8', + 130041 => '9', + 194560 => '丽', + 194561 => '丸', + 194562 => '乁', + 194563 => '𠄢', + 194564 => '你', + 194565 => '侮', + 194566 => '侻', + 194567 => '倂', + 194568 => '偺', + 194569 => '備', + 194570 => '僧', + 194571 => '像', + 194572 => '㒞', + 194573 => '𠘺', + 194574 => '免', + 194575 => '兔', + 194576 => '兤', + 194577 => '具', + 194578 => '𠔜', + 194579 => '㒹', + 194580 => '內', + 194581 => '再', + 194582 => '𠕋', + 194583 => '冗', + 194584 => '冤', + 194585 => '仌', + 194586 => '冬', + 194587 => '况', + 194588 => '𩇟', + 194589 => '凵', + 194590 => '刃', + 194591 => '㓟', + 194592 => '刻', + 194593 => '剆', + 194594 => '割', + 194595 => '剷', + 194596 => '㔕', + 194597 => '勇', + 194598 => '勉', + 194599 => '勤', + 194600 => '勺', + 194601 => '包', + 194602 => '匆', + 194603 => '北', + 194604 => '卉', + 194605 => '卑', + 194606 => '博', + 194607 => '即', + 194608 => '卽', + 194609 => '卿', + 194610 => '卿', + 194611 => '卿', + 194612 => '𠨬', + 194613 => '灰', + 194614 => '及', + 194615 => '叟', + 194616 => '𠭣', + 194617 => '叫', + 194618 => '叱', + 194619 => '吆', + 194620 => '咞', + 194621 => '吸', + 194622 => '呈', + 194623 => '周', + 194624 => '咢', + 194625 => '哶', + 194626 => '唐', + 194627 => '啓', + 194628 => '啣', + 194629 => '善', + 194630 => '善', + 194631 => '喙', + 194632 => '喫', + 194633 => '喳', + 194634 => '嗂', + 194635 => '圖', + 194636 => '嘆', + 194637 => '圗', + 194638 => '噑', + 194639 => '噴', + 194640 => '切', + 194641 => '壮', + 194642 => '城', + 194643 => '埴', + 194644 => '堍', + 194645 => '型', + 194646 => '堲', + 194647 => '報', + 194648 => '墬', + 194649 => '𡓤', + 194650 => '売', + 194651 => '壷', + 194652 => '夆', + 194653 => '多', + 194654 => '夢', + 194655 => '奢', + 194656 => '𡚨', + 194657 => '𡛪', + 194658 => '姬', + 194659 => '娛', + 194660 => '娧', + 194661 => '姘', + 194662 => '婦', + 194663 => '㛮', + 194665 => '嬈', + 194666 => '嬾', + 194667 => '嬾', + 194668 => '𡧈', + 194669 => '寃', + 194670 => '寘', + 194671 => '寧', + 194672 => '寳', + 194673 => '𡬘', + 194674 => '寿', + 194675 => '将', + 194677 => '尢', + 194678 => '㞁', + 194679 => '屠', + 194680 => '屮', + 194681 => '峀', + 194682 => '岍', + 194683 => '𡷤', + 194684 => '嵃', + 194685 => '𡷦', + 194686 => '嵮', + 194687 => '嵫', + 194688 => '嵼', + 194689 => '巡', + 194690 => '巢', + 194691 => '㠯', + 194692 => '巽', + 194693 => '帨', + 194694 => '帽', + 194695 => '幩', + 194696 => '㡢', + 194697 => '𢆃', + 194698 => '㡼', + 194699 => '庰', + 194700 => '庳', + 194701 => '庶', + 194702 => '廊', + 194703 => '𪎒', + 194704 => '廾', + 194705 => '𢌱', + 194706 => '𢌱', + 194707 => '舁', + 194708 => '弢', + 194709 => '弢', + 194710 => '㣇', + 194711 => '𣊸', + 194712 => '𦇚', + 194713 => '形', + 194714 => '彫', + 194715 => '㣣', + 194716 => '徚', + 194717 => '忍', + 194718 => '志', + 194719 => '忹', + 194720 => '悁', + 194721 => '㤺', + 194722 => '㤜', + 194723 => '悔', + 194724 => '𢛔', + 194725 => '惇', + 194726 => '慈', + 194727 => '慌', + 194728 => '慎', + 194729 => '慌', + 194730 => '慺', + 194731 => '憎', + 194732 => '憲', + 194733 => '憤', + 194734 => '憯', + 194735 => '懞', + 194736 => '懲', + 194737 => '懶', + 194738 => '成', + 194739 => '戛', + 194740 => '扝', + 194741 => '抱', + 194742 => '拔', + 194743 => '捐', + 194744 => '𢬌', + 194745 => '挽', + 194746 => '拼', + 194747 => '捨', + 194748 => '掃', + 194749 => '揤', + 194750 => '𢯱', + 194751 => '搢', + 194752 => '揅', + 194753 => '掩', + 194754 => '㨮', + 194755 => '摩', + 194756 => '摾', + 194757 => '撝', + 194758 => '摷', + 194759 => '㩬', + 194760 => '敏', + 194761 => '敬', + 194762 => '𣀊', + 194763 => '旣', + 194764 => '書', + 194765 => '晉', + 194766 => '㬙', + 194767 => '暑', + 194768 => '㬈', + 194769 => '㫤', + 194770 => '冒', + 194771 => '冕', + 194772 => '最', + 194773 => '暜', + 194774 => '肭', + 194775 => '䏙', + 194776 => '朗', + 194777 => '望', + 194778 => '朡', + 194779 => '杞', + 194780 => '杓', + 194781 => '𣏃', + 194782 => '㭉', + 194783 => '柺', + 194784 => '枅', + 194785 => '桒', + 194786 => '梅', + 194787 => '𣑭', + 194788 => '梎', + 194789 => '栟', + 194790 => '椔', + 194791 => '㮝', + 194792 => '楂', + 194793 => '榣', + 194794 => '槪', + 194795 => '檨', + 194796 => '𣚣', + 194797 => '櫛', + 194798 => '㰘', + 194799 => '次', + 194800 => '𣢧', + 194801 => '歔', + 194802 => '㱎', + 194803 => '歲', + 194804 => '殟', + 194805 => '殺', + 194806 => '殻', + 194807 => '𣪍', + 194808 => '𡴋', + 194809 => '𣫺', + 194810 => '汎', + 194811 => '𣲼', + 194812 => '沿', + 194813 => '泍', + 194814 => '汧', + 194815 => '洖', + 194816 => '派', + 194817 => '海', + 194818 => '流', + 194819 => '浩', + 194820 => '浸', + 194821 => '涅', + 194822 => '𣴞', + 194823 => '洴', + 194824 => '港', + 194825 => '湮', + 194826 => '㴳', + 194827 => '滋', + 194828 => '滇', + 194829 => '𣻑', + 194830 => '淹', + 194831 => '潮', + 194832 => '𣽞', + 194833 => '𣾎', + 194834 => '濆', + 194835 => '瀹', + 194836 => '瀞', + 194837 => '瀛', + 194838 => '㶖', + 194839 => '灊', + 194840 => '災', + 194841 => '灷', + 194842 => '炭', + 194843 => '𠔥', + 194844 => '煅', + 194845 => '𤉣', + 194846 => '熜', + 194848 => '爨', + 194849 => '爵', + 194850 => '牐', + 194851 => '𤘈', + 194852 => '犀', + 194853 => '犕', + 194854 => '𤜵', + 194855 => '𤠔', + 194856 => '獺', + 194857 => '王', + 194858 => '㺬', + 194859 => '玥', + 194860 => '㺸', + 194861 => '㺸', + 194862 => '瑇', + 194863 => '瑜', + 194864 => '瑱', + 194865 => '璅', + 194866 => '瓊', + 194867 => '㼛', + 194868 => '甤', + 194869 => '𤰶', + 194870 => '甾', + 194871 => '𤲒', + 194872 => '異', + 194873 => '𢆟', + 194874 => '瘐', + 194875 => '𤾡', + 194876 => '𤾸', + 194877 => '𥁄', + 194878 => '㿼', + 194879 => '䀈', + 194880 => '直', + 194881 => '𥃳', + 194882 => '𥃲', + 194883 => '𥄙', + 194884 => '𥄳', + 194885 => '眞', + 194886 => '真', + 194887 => '真', + 194888 => '睊', + 194889 => '䀹', + 194890 => '瞋', + 194891 => '䁆', + 194892 => '䂖', + 194893 => '𥐝', + 194894 => '硎', + 194895 => '碌', + 194896 => '磌', + 194897 => '䃣', + 194898 => '𥘦', + 194899 => '祖', + 194900 => '𥚚', + 194901 => '𥛅', + 194902 => '福', + 194903 => '秫', + 194904 => '䄯', + 194905 => '穀', + 194906 => '穊', + 194907 => '穏', + 194908 => '𥥼', + 194909 => '𥪧', + 194910 => '𥪧', + 194912 => '䈂', + 194913 => '𥮫', + 194914 => '篆', + 194915 => '築', + 194916 => '䈧', + 194917 => '𥲀', + 194918 => '糒', + 194919 => '䊠', + 194920 => '糨', + 194921 => '糣', + 194922 => '紀', + 194923 => '𥾆', + 194924 => '絣', + 194925 => '䌁', + 194926 => '緇', + 194927 => '縂', + 194928 => '繅', + 194929 => '䌴', + 194930 => '𦈨', + 194931 => '𦉇', + 194932 => '䍙', + 194933 => '𦋙', + 194934 => '罺', + 194935 => '𦌾', + 194936 => '羕', + 194937 => '翺', + 194938 => '者', + 194939 => '𦓚', + 194940 => '𦔣', + 194941 => '聠', + 194942 => '𦖨', + 194943 => '聰', + 194944 => '𣍟', + 194945 => '䏕', + 194946 => '育', + 194947 => '脃', + 194948 => '䐋', + 194949 => '脾', + 194950 => '媵', + 194951 => '𦞧', + 194952 => '𦞵', + 194953 => '𣎓', + 194954 => '𣎜', + 194955 => '舁', + 194956 => '舄', + 194957 => '辞', + 194958 => '䑫', + 194959 => '芑', + 194960 => '芋', + 194961 => '芝', + 194962 => '劳', + 194963 => '花', + 194964 => '芳', + 194965 => '芽', + 194966 => '苦', + 194967 => '𦬼', + 194968 => '若', + 194969 => '茝', + 194970 => '荣', + 194971 => '莭', + 194972 => '茣', + 194973 => '莽', + 194974 => '菧', + 194975 => '著', + 194976 => '荓', + 194977 => '菊', + 194978 => '菌', + 194979 => '菜', + 194980 => '𦰶', + 194981 => '𦵫', + 194982 => '𦳕', + 194983 => '䔫', + 194984 => '蓱', + 194985 => '蓳', + 194986 => '蔖', + 194987 => '𧏊', + 194988 => '蕤', + 194989 => '𦼬', + 194990 => '䕝', + 194991 => '䕡', + 194992 => '𦾱', + 194993 => '𧃒', + 194994 => '䕫', + 194995 => '虐', + 194996 => '虜', + 194997 => '虧', + 194998 => '虩', + 194999 => '蚩', + 195000 => '蚈', + 195001 => '蜎', + 195002 => '蛢', + 195003 => '蝹', + 195004 => '蜨', + 195005 => '蝫', + 195006 => '螆', + 195008 => '蟡', + 195009 => '蠁', + 195010 => '䗹', + 195011 => '衠', + 195012 => '衣', + 195013 => '𧙧', + 195014 => '裗', + 195015 => '裞', + 195016 => '䘵', + 195017 => '裺', + 195018 => '㒻', + 195019 => '𧢮', + 195020 => '𧥦', + 195021 => '䚾', + 195022 => '䛇', + 195023 => '誠', + 195024 => '諭', + 195025 => '變', + 195026 => '豕', + 195027 => '𧲨', + 195028 => '貫', + 195029 => '賁', + 195030 => '贛', + 195031 => '起', + 195032 => '𧼯', + 195033 => '𠠄', + 195034 => '跋', + 195035 => '趼', + 195036 => '跰', + 195037 => '𠣞', + 195038 => '軔', + 195039 => '輸', + 195040 => '𨗒', + 195041 => '𨗭', + 195042 => '邔', + 195043 => '郱', + 195044 => '鄑', + 195045 => '𨜮', + 195046 => '鄛', + 195047 => '鈸', + 195048 => '鋗', + 195049 => '鋘', + 195050 => '鉼', + 195051 => '鏹', + 195052 => '鐕', + 195053 => '𨯺', + 195054 => '開', + 195055 => '䦕', + 195056 => '閷', + 195057 => '𨵷', + 195058 => '䧦', + 195059 => '雃', + 195060 => '嶲', + 195061 => '霣', + 195062 => '𩅅', + 195063 => '𩈚', + 195064 => '䩮', + 195065 => '䩶', + 195066 => '韠', + 195067 => '𩐊', + 195068 => '䪲', + 195069 => '𩒖', + 195070 => '頋', + 195071 => '頋', + 195072 => '頩', + 195073 => '𩖶', + 195074 => '飢', + 195075 => '䬳', + 195076 => '餩', + 195077 => '馧', + 195078 => '駂', + 195079 => '駾', + 195080 => '䯎', + 195081 => '𩬰', + 195082 => '鬒', + 195083 => '鱀', + 195084 => '鳽', + 195085 => '䳎', + 195086 => '䳭', + 195087 => '鵧', + 195088 => '𪃎', + 195089 => '䳸', + 195090 => '𪄅', + 195091 => '𪈎', + 195092 => '𪊑', + 195093 => '麻', + 195094 => '䵖', + 195095 => '黹', + 195096 => '黾', + 195097 => '鼅', + 195098 => '鼏', + 195099 => '鼖', + 195100 => '鼻', + 195101 => '𪘀', +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php new file mode 100644 index 0000000..1958e37 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php @@ -0,0 +1,65 @@ + 9, + 2509 => 9, + 2637 => 9, + 2765 => 9, + 2893 => 9, + 3021 => 9, + 3149 => 9, + 3277 => 9, + 3387 => 9, + 3388 => 9, + 3405 => 9, + 3530 => 9, + 3642 => 9, + 3770 => 9, + 3972 => 9, + 4153 => 9, + 4154 => 9, + 5908 => 9, + 5940 => 9, + 6098 => 9, + 6752 => 9, + 6980 => 9, + 7082 => 9, + 7083 => 9, + 7154 => 9, + 7155 => 9, + 11647 => 9, + 43014 => 9, + 43052 => 9, + 43204 => 9, + 43347 => 9, + 43456 => 9, + 43766 => 9, + 44013 => 9, + 68159 => 9, + 69702 => 9, + 69759 => 9, + 69817 => 9, + 69939 => 9, + 69940 => 9, + 70080 => 9, + 70197 => 9, + 70378 => 9, + 70477 => 9, + 70722 => 9, + 70850 => 9, + 71103 => 9, + 71231 => 9, + 71350 => 9, + 71467 => 9, + 71737 => 9, + 71997 => 9, + 71998 => 9, + 72160 => 9, + 72244 => 9, + 72263 => 9, + 72345 => 9, + 72767 => 9, + 73028 => 9, + 73029 => 9, + 73111 => 9, +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap.php b/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap.php new file mode 100644 index 0000000..57c7835 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (extension_loaded('intl')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!defined('U_IDNA_PROHIBITED_ERROR')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); +} +if (!defined('U_IDNA_ERROR_START')) { + define('U_IDNA_ERROR_START', 66560); +} +if (!defined('U_IDNA_UNASSIGNED_ERROR')) { + define('U_IDNA_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_IDNA_CHECK_BIDI_ERROR')) { + define('U_IDNA_CHECK_BIDI_ERROR', 66562); +} +if (!defined('U_IDNA_STD3_ASCII_RULES_ERROR')) { + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); +} +if (!defined('U_IDNA_ACE_PREFIX_ERROR')) { + define('U_IDNA_ACE_PREFIX_ERROR', 66564); +} +if (!defined('U_IDNA_VERIFICATION_ERROR')) { + define('U_IDNA_VERIFICATION_ERROR', 66565); +} +if (!defined('U_IDNA_LABEL_TOO_LONG_ERROR')) { + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); +} +if (!defined('U_IDNA_ZERO_LENGTH_LABEL_ERROR')) { + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); +} +if (!defined('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR')) { + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); +} +if (!defined('U_IDNA_ERROR_LIMIT')) { + define('U_IDNA_ERROR_LIMIT', 66569); +} +if (!defined('U_STRINGPREP_PROHIBITED_ERROR')) { + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); +} +if (!defined('U_STRINGPREP_UNASSIGNED_ERROR')) { + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_STRINGPREP_CHECK_BIDI_ERROR')) { + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); +} +if (!defined('IDNA_DEFAULT')) { + define('IDNA_DEFAULT', 0); +} +if (!defined('IDNA_ALLOW_UNASSIGNED')) { + define('IDNA_ALLOW_UNASSIGNED', 1); +} +if (!defined('IDNA_USE_STD3_RULES')) { + define('IDNA_USE_STD3_RULES', 2); +} +if (!defined('IDNA_CHECK_BIDI')) { + define('IDNA_CHECK_BIDI', 4); +} +if (!defined('IDNA_CHECK_CONTEXTJ')) { + define('IDNA_CHECK_CONTEXTJ', 8); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_ASCII')) { + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_UNICODE')) { + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); +} +if (!defined('INTL_IDNA_VARIANT_2003')) { + define('INTL_IDNA_VARIANT_2003', 0); +} +if (!defined('INTL_IDNA_VARIANT_UTS46')) { + define('INTL_IDNA_VARIANT_UTS46', 1); +} +if (!defined('IDNA_ERROR_EMPTY_LABEL')) { + define('IDNA_ERROR_EMPTY_LABEL', 1); +} +if (!defined('IDNA_ERROR_LABEL_TOO_LONG')) { + define('IDNA_ERROR_LABEL_TOO_LONG', 2); +} +if (!defined('IDNA_ERROR_DOMAIN_NAME_TOO_LONG')) { + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); +} +if (!defined('IDNA_ERROR_LEADING_HYPHEN')) { + define('IDNA_ERROR_LEADING_HYPHEN', 8); +} +if (!defined('IDNA_ERROR_TRAILING_HYPHEN')) { + define('IDNA_ERROR_TRAILING_HYPHEN', 16); +} +if (!defined('IDNA_ERROR_HYPHEN_3_4')) { + define('IDNA_ERROR_HYPHEN_3_4', 32); +} +if (!defined('IDNA_ERROR_LEADING_COMBINING_MARK')) { + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); +} +if (!defined('IDNA_ERROR_DISALLOWED')) { + define('IDNA_ERROR_DISALLOWED', 128); +} +if (!defined('IDNA_ERROR_PUNYCODE')) { + define('IDNA_ERROR_PUNYCODE', 256); +} +if (!defined('IDNA_ERROR_LABEL_HAS_DOT')) { + define('IDNA_ERROR_LABEL_HAS_DOT', 512); +} +if (!defined('IDNA_ERROR_INVALID_ACE_LABEL')) { + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); +} +if (!defined('IDNA_ERROR_BIDI')) { + define('IDNA_ERROR_BIDI', 2048); +} +if (!defined('IDNA_ERROR_CONTEXTJ')) { + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (\PHP_VERSION_ID < 70400) { + if (!function_exists('idn_to_ascii')) { + function idn_to_ascii($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_2003, &$idna_info = null) { return p\Idn::idn_to_ascii($domain, $flags, $variant, $idna_info); } + } + if (!function_exists('idn_to_utf8')) { + function idn_to_utf8($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_2003, &$idna_info = null) { return p\Idn::idn_to_utf8($domain, $flags, $variant, $idna_info); } + } +} else { + if (!function_exists('idn_to_ascii')) { + function idn_to_ascii($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_UTS46, &$idna_info = null) { return p\Idn::idn_to_ascii($domain, $flags, $variant, $idna_info); } + } + if (!function_exists('idn_to_utf8')) { + function idn_to_utf8($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_UTS46, &$idna_info = null) { return p\Idn::idn_to_utf8($domain, $flags, $variant, $idna_info); } + } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap80.php b/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap80.php new file mode 100644 index 0000000..a62c2d6 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap80.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (!defined('U_IDNA_PROHIBITED_ERROR')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); +} +if (!defined('U_IDNA_ERROR_START')) { + define('U_IDNA_ERROR_START', 66560); +} +if (!defined('U_IDNA_UNASSIGNED_ERROR')) { + define('U_IDNA_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_IDNA_CHECK_BIDI_ERROR')) { + define('U_IDNA_CHECK_BIDI_ERROR', 66562); +} +if (!defined('U_IDNA_STD3_ASCII_RULES_ERROR')) { + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); +} +if (!defined('U_IDNA_ACE_PREFIX_ERROR')) { + define('U_IDNA_ACE_PREFIX_ERROR', 66564); +} +if (!defined('U_IDNA_VERIFICATION_ERROR')) { + define('U_IDNA_VERIFICATION_ERROR', 66565); +} +if (!defined('U_IDNA_LABEL_TOO_LONG_ERROR')) { + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); +} +if (!defined('U_IDNA_ZERO_LENGTH_LABEL_ERROR')) { + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); +} +if (!defined('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR')) { + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); +} +if (!defined('U_IDNA_ERROR_LIMIT')) { + define('U_IDNA_ERROR_LIMIT', 66569); +} +if (!defined('U_STRINGPREP_PROHIBITED_ERROR')) { + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); +} +if (!defined('U_STRINGPREP_UNASSIGNED_ERROR')) { + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_STRINGPREP_CHECK_BIDI_ERROR')) { + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); +} +if (!defined('IDNA_DEFAULT')) { + define('IDNA_DEFAULT', 0); +} +if (!defined('IDNA_ALLOW_UNASSIGNED')) { + define('IDNA_ALLOW_UNASSIGNED', 1); +} +if (!defined('IDNA_USE_STD3_RULES')) { + define('IDNA_USE_STD3_RULES', 2); +} +if (!defined('IDNA_CHECK_BIDI')) { + define('IDNA_CHECK_BIDI', 4); +} +if (!defined('IDNA_CHECK_CONTEXTJ')) { + define('IDNA_CHECK_CONTEXTJ', 8); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_ASCII')) { + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_UNICODE')) { + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); +} +if (!defined('INTL_IDNA_VARIANT_UTS46')) { + define('INTL_IDNA_VARIANT_UTS46', 1); +} +if (!defined('IDNA_ERROR_EMPTY_LABEL')) { + define('IDNA_ERROR_EMPTY_LABEL', 1); +} +if (!defined('IDNA_ERROR_LABEL_TOO_LONG')) { + define('IDNA_ERROR_LABEL_TOO_LONG', 2); +} +if (!defined('IDNA_ERROR_DOMAIN_NAME_TOO_LONG')) { + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); +} +if (!defined('IDNA_ERROR_LEADING_HYPHEN')) { + define('IDNA_ERROR_LEADING_HYPHEN', 8); +} +if (!defined('IDNA_ERROR_TRAILING_HYPHEN')) { + define('IDNA_ERROR_TRAILING_HYPHEN', 16); +} +if (!defined('IDNA_ERROR_HYPHEN_3_4')) { + define('IDNA_ERROR_HYPHEN_3_4', 32); +} +if (!defined('IDNA_ERROR_LEADING_COMBINING_MARK')) { + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); +} +if (!defined('IDNA_ERROR_DISALLOWED')) { + define('IDNA_ERROR_DISALLOWED', 128); +} +if (!defined('IDNA_ERROR_PUNYCODE')) { + define('IDNA_ERROR_PUNYCODE', 256); +} +if (!defined('IDNA_ERROR_LABEL_HAS_DOT')) { + define('IDNA_ERROR_LABEL_HAS_DOT', 512); +} +if (!defined('IDNA_ERROR_INVALID_ACE_LABEL')) { + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); +} +if (!defined('IDNA_ERROR_BIDI')) { + define('IDNA_ERROR_BIDI', 2048); +} +if (!defined('IDNA_ERROR_CONTEXTJ')) { + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (!function_exists('idn_to_ascii')) { + function idn_to_ascii(?string $domain, ?int $flags = IDNA_DEFAULT, ?int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = null): string|false { return p\Idn::idn_to_ascii((string) $domain, (int) $flags, (int) $variant, $idna_info); } +} +if (!function_exists('idn_to_utf8')) { + function idn_to_utf8(?string $domain, ?int $flags = IDNA_DEFAULT, ?int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = null): string|false { return p\Idn::idn_to_utf8((string) $domain, (int) $flags, (int) $variant, $idna_info); } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-idn/composer.json b/plugins/email/vendor/symfony/polyfill-intl-idn/composer.json new file mode 100644 index 0000000..71030a2 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-idn/composer.json @@ -0,0 +1,44 @@ +{ + "name": "symfony/polyfill-intl-idn", + "type": "library", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "idn"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\Idn\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/LICENSE b/plugins/email/vendor/symfony/polyfill-intl-normalizer/LICENSE new file mode 100644 index 0000000..4cd8bdd --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/Normalizer.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Normalizer.php new file mode 100644 index 0000000..4443c23 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Normalizer.php @@ -0,0 +1,310 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Normalizer; + +/** + * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension. + * + * It has been validated with Unicode 6.3 Normalization Conformance Test. + * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations. + * + * @author Nicolas Grekas + * + * @internal + */ +class Normalizer +{ + public const FORM_D = \Normalizer::FORM_D; + public const FORM_KD = \Normalizer::FORM_KD; + public const FORM_C = \Normalizer::FORM_C; + public const FORM_KC = \Normalizer::FORM_KC; + public const NFD = \Normalizer::NFD; + public const NFKD = \Normalizer::NFKD; + public const NFC = \Normalizer::NFC; + public const NFKC = \Normalizer::NFKC; + + private static $C; + private static $D; + private static $KD; + private static $cC; + private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; + + public static function isNormalized(string $s, int $form = self::FORM_C) + { + if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) { + return false; + } + if (!isset($s[strspn($s, self::$ASCII)])) { + return true; + } + if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) { + return true; + } + + return self::normalize($s, $form) === $s; + } + + public static function normalize(string $s, int $form = self::FORM_C) + { + if (!preg_match('//u', $s)) { + return false; + } + + switch ($form) { + case self::NFC: $C = true; $K = false; break; + case self::NFD: $C = false; $K = false; break; + case self::NFKC: $C = true; $K = true; break; + case self::NFKD: $C = false; $K = true; break; + default: + if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) { + return $s; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form'); + } + + if ('' === $s) { + return ''; + } + + if ($K && null === self::$KD) { + self::$KD = self::getData('compatibilityDecomposition'); + } + + if (null === self::$D) { + self::$D = self::getData('canonicalDecomposition'); + self::$cC = self::getData('combiningClass'); + } + + if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) { + mb_internal_encoding('8bit'); + } + + $r = self::decompose($s, $K); + + if ($C) { + if (null === self::$C) { + self::$C = self::getData('canonicalComposition'); + } + + $r = self::recompose($r); + } + if (null !== $mbEncoding) { + mb_internal_encoding($mbEncoding); + } + + return $r; + } + + private static function recompose($s) + { + $ASCII = self::$ASCII; + $compMap = self::$C; + $combClass = self::$cC; + $ulenMask = self::$ulenMask; + + $result = $tail = ''; + + $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"]; + $len = \strlen($s); + + $lastUchr = substr($s, 0, $i); + $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0; + + while ($i < $len) { + if ($s[$i] < "\x80") { + // ASCII chars + + if ($tail) { + $lastUchr .= $tail; + $tail = ''; + } + + if ($j = strspn($s, $ASCII, $i + 1)) { + $lastUchr .= substr($s, $i, $j); + $i += $j; + } + + $result .= $lastUchr; + $lastUchr = $s[$i]; + $lastUcls = 0; + ++$i; + continue; + } + + $ulen = $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + + if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr + || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr + || $lastUcls) { + // Table lookup and combining chars composition + + $ucls = $combClass[$uchr] ?? 0; + + if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) { + $lastUchr = $compMap[$lastUchr.$uchr]; + } elseif ($lastUcls = $ucls) { + $tail .= $uchr; + } else { + if ($tail) { + $lastUchr .= $tail; + $tail = ''; + } + + $result .= $lastUchr; + $lastUchr = $uchr; + } + } else { + // Hangul chars + + $L = \ord($lastUchr[2]) - 0x80; + $V = \ord($uchr[2]) - 0xA1; + $T = 0; + + $uchr = substr($s, $i + $ulen, 3); + + if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") { + $T = \ord($uchr[2]) - 0xA7; + 0 > $T && $T += 0x40; + $ulen += 3; + } + + $L = 0xAC00 + ($L * 21 + $V) * 28 + $T; + $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F); + } + + $i += $ulen; + } + + return $result.$lastUchr.$tail; + } + + private static function decompose($s, $c) + { + $result = ''; + + $ASCII = self::$ASCII; + $decompMap = self::$D; + $combClass = self::$cC; + $ulenMask = self::$ulenMask; + if ($c) { + $compatMap = self::$KD; + } + + $c = []; + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + if ($s[$i] < "\x80") { + // ASCII chars + + if ($c) { + ksort($c); + $result .= implode('', $c); + $c = []; + } + + $j = 1 + strspn($s, $ASCII, $i + 1); + $result .= substr($s, $i, $j); + $i += $j; + continue; + } + + $ulen = $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) { + // Table lookup + + if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) { + $uchr = $j; + + $j = \strlen($uchr); + $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"]; + + if ($ulen != $j) { + // Put trailing chars in $s + + $j -= $ulen; + $i -= $j; + + if (0 > $i) { + $s = str_repeat(' ', -$i).$s; + $len -= $i; + $i = 0; + } + + while ($j--) { + $s[$i + $j] = $uchr[$ulen + $j]; + } + + $uchr = substr($uchr, 0, $ulen); + } + } + if (isset($combClass[$uchr])) { + // Combining chars, for sorting + + if (!isset($c[$combClass[$uchr]])) { + $c[$combClass[$uchr]] = ''; + } + $c[$combClass[$uchr]] .= $uchr; + continue; + } + } else { + // Hangul chars + + $uchr = unpack('C*', $uchr); + $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80; + + $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588)) + ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28)); + + if ($j %= 28) { + $uchr .= $j < 25 + ? ("\xE1\x86".\chr(0xA7 + $j)) + : ("\xE1\x87".\chr(0x67 + $j)); + } + } + if ($c) { + ksort($c); + $result .= implode('', $c); + $c = []; + } + + $result .= $uchr; + } + + if ($c) { + ksort($c); + $result .= implode('', $c); + } + + return $result; + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/README.md b/plugins/email/vendor/symfony/polyfill-intl-normalizer/README.md new file mode 100644 index 0000000..b9b762e --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/README.md @@ -0,0 +1,14 @@ +Symfony Polyfill / Intl: Normalizer +=================================== + +This component provides a fallback implementation for the +[`Normalizer`](https://php.net/Normalizer) class provided +by the [Intl](https://php.net/intl) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php new file mode 100644 index 0000000..0fdfc89 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php @@ -0,0 +1,17 @@ + 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ĕ' => 'Ĕ', + 'ĕ' => 'ĕ', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'ģ' => 'ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Ĭ' => 'Ĭ', + 'ĭ' => 'ĭ', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ŏ' => 'Ŏ', + 'ŏ' => 'ŏ', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'Ơ' => 'Ơ', + 'ơ' => 'ơ', + 'Ư' => 'Ư', + 'ư' => 'ư', + 'Ǎ' => 'Ǎ', + 'ǎ' => 'ǎ', + 'Ǐ' => 'Ǐ', + 'ǐ' => 'ǐ', + 'Ǒ' => 'Ǒ', + 'ǒ' => 'ǒ', + 'Ǔ' => 'Ǔ', + 'ǔ' => 'ǔ', + 'Ǖ' => 'Ǖ', + 'ǖ' => 'ǖ', + 'Ǘ' => 'Ǘ', + 'ǘ' => 'ǘ', + 'Ǚ' => 'Ǚ', + 'ǚ' => 'ǚ', + 'Ǜ' => 'Ǜ', + 'ǜ' => 'ǜ', + 'Ǟ' => 'Ǟ', + 'ǟ' => 'ǟ', + 'Ǡ' => 'Ǡ', + 'ǡ' => 'ǡ', + 'Ǣ' => 'Ǣ', + 'ǣ' => 'ǣ', + 'Ǧ' => 'Ǧ', + 'ǧ' => 'ǧ', + 'Ǩ' => 'Ǩ', + 'ǩ' => 'ǩ', + 'Ǫ' => 'Ǫ', + 'ǫ' => 'ǫ', + 'Ǭ' => 'Ǭ', + 'ǭ' => 'ǭ', + 'Ǯ' => 'Ǯ', + 'ǯ' => 'ǯ', + 'ǰ' => 'ǰ', + 'Ǵ' => 'Ǵ', + 'ǵ' => 'ǵ', + 'Ǹ' => 'Ǹ', + 'ǹ' => 'ǹ', + 'Ǻ' => 'Ǻ', + 'ǻ' => 'ǻ', + 'Ǽ' => 'Ǽ', + 'ǽ' => 'ǽ', + 'Ǿ' => 'Ǿ', + 'ǿ' => 'ǿ', + 'Ȁ' => 'Ȁ', + 'ȁ' => 'ȁ', + 'Ȃ' => 'Ȃ', + 'ȃ' => 'ȃ', + 'Ȅ' => 'Ȅ', + 'ȅ' => 'ȅ', + 'Ȇ' => 'Ȇ', + 'ȇ' => 'ȇ', + 'Ȉ' => 'Ȉ', + 'ȉ' => 'ȉ', + 'Ȋ' => 'Ȋ', + 'ȋ' => 'ȋ', + 'Ȍ' => 'Ȍ', + 'ȍ' => 'ȍ', + 'Ȏ' => 'Ȏ', + 'ȏ' => 'ȏ', + 'Ȑ' => 'Ȑ', + 'ȑ' => 'ȑ', + 'Ȓ' => 'Ȓ', + 'ȓ' => 'ȓ', + 'Ȕ' => 'Ȕ', + 'ȕ' => 'ȕ', + 'Ȗ' => 'Ȗ', + 'ȗ' => 'ȗ', + 'Ș' => 'Ș', + 'ș' => 'ș', + 'Ț' => 'Ț', + 'ț' => 'ț', + 'Ȟ' => 'Ȟ', + 'ȟ' => 'ȟ', + 'Ȧ' => 'Ȧ', + 'ȧ' => 'ȧ', + 'Ȩ' => 'Ȩ', + 'ȩ' => 'ȩ', + 'Ȫ' => 'Ȫ', + 'ȫ' => 'ȫ', + 'Ȭ' => 'Ȭ', + 'ȭ' => 'ȭ', + 'Ȯ' => 'Ȯ', + 'ȯ' => 'ȯ', + 'Ȱ' => 'Ȱ', + 'ȱ' => 'ȱ', + 'Ȳ' => 'Ȳ', + 'ȳ' => 'ȳ', + '΅' => '΅', + 'Ά' => 'Ά', + 'Έ' => 'Έ', + 'Ή' => 'Ή', + 'Ί' => 'Ί', + 'Ό' => 'Ό', + 'Ύ' => 'Ύ', + 'Ώ' => 'Ώ', + 'ΐ' => 'ΐ', + 'Ϊ' => 'Ϊ', + 'Ϋ' => 'Ϋ', + 'ά' => 'ά', + 'έ' => 'έ', + 'ή' => 'ή', + 'ί' => 'ί', + 'ΰ' => 'ΰ', + 'ϊ' => 'ϊ', + 'ϋ' => 'ϋ', + 'ό' => 'ό', + 'ύ' => 'ύ', + 'ώ' => 'ώ', + 'ϓ' => 'ϓ', + 'ϔ' => 'ϔ', + 'Ѐ' => 'Ѐ', + 'Ё' => 'Ё', + 'Ѓ' => 'Ѓ', + 'Ї' => 'Ї', + 'Ќ' => 'Ќ', + 'Ѝ' => 'Ѝ', + 'Ў' => 'Ў', + 'Й' => 'Й', + 'й' => 'й', + 'ѐ' => 'ѐ', + 'ё' => 'ё', + 'ѓ' => 'ѓ', + 'ї' => 'ї', + 'ќ' => 'ќ', + 'ѝ' => 'ѝ', + 'ў' => 'ў', + 'Ѷ' => 'Ѷ', + 'ѷ' => 'ѷ', + 'Ӂ' => 'Ӂ', + 'ӂ' => 'ӂ', + 'Ӑ' => 'Ӑ', + 'ӑ' => 'ӑ', + 'Ӓ' => 'Ӓ', + 'ӓ' => 'ӓ', + 'Ӗ' => 'Ӗ', + 'ӗ' => 'ӗ', + 'Ӛ' => 'Ӛ', + 'ӛ' => 'ӛ', + 'Ӝ' => 'Ӝ', + 'ӝ' => 'ӝ', + 'Ӟ' => 'Ӟ', + 'ӟ' => 'ӟ', + 'Ӣ' => 'Ӣ', + 'ӣ' => 'ӣ', + 'Ӥ' => 'Ӥ', + 'ӥ' => 'ӥ', + 'Ӧ' => 'Ӧ', + 'ӧ' => 'ӧ', + 'Ӫ' => 'Ӫ', + 'ӫ' => 'ӫ', + 'Ӭ' => 'Ӭ', + 'ӭ' => 'ӭ', + 'Ӯ' => 'Ӯ', + 'ӯ' => 'ӯ', + 'Ӱ' => 'Ӱ', + 'ӱ' => 'ӱ', + 'Ӳ' => 'Ӳ', + 'ӳ' => 'ӳ', + 'Ӵ' => 'Ӵ', + 'ӵ' => 'ӵ', + 'Ӹ' => 'Ӹ', + 'ӹ' => 'ӹ', + 'آ' => 'آ', + 'أ' => 'أ', + 'ؤ' => 'ؤ', + 'إ' => 'إ', + 'ئ' => 'ئ', + 'ۀ' => 'ۀ', + 'ۂ' => 'ۂ', + 'ۓ' => 'ۓ', + 'ऩ' => 'ऩ', + 'ऱ' => 'ऱ', + 'ऴ' => 'ऴ', + 'ো' => 'ো', + 'ৌ' => 'ৌ', + 'ୈ' => 'ୈ', + 'ୋ' => 'ୋ', + 'ୌ' => 'ୌ', + 'ஔ' => 'ஔ', + 'ொ' => 'ொ', + 'ோ' => 'ோ', + 'ௌ' => 'ௌ', + 'ై' => 'ై', + 'ೀ' => 'ೀ', + 'ೇ' => 'ೇ', + 'ೈ' => 'ೈ', + 'ೊ' => 'ೊ', + 'ೋ' => 'ೋ', + 'ൊ' => 'ൊ', + 'ോ' => 'ോ', + 'ൌ' => 'ൌ', + 'ේ' => 'ේ', + 'ො' => 'ො', + 'ෝ' => 'ෝ', + 'ෞ' => 'ෞ', + 'ဦ' => 'ဦ', + 'ᬆ' => 'ᬆ', + 'ᬈ' => 'ᬈ', + 'ᬊ' => 'ᬊ', + 'ᬌ' => 'ᬌ', + 'ᬎ' => 'ᬎ', + 'ᬒ' => 'ᬒ', + 'ᬻ' => 'ᬻ', + 'ᬽ' => 'ᬽ', + 'ᭀ' => 'ᭀ', + 'ᭁ' => 'ᭁ', + 'ᭃ' => 'ᭃ', + 'Ḁ' => 'Ḁ', + 'ḁ' => 'ḁ', + 'Ḃ' => 'Ḃ', + 'ḃ' => 'ḃ', + 'Ḅ' => 'Ḅ', + 'ḅ' => 'ḅ', + 'Ḇ' => 'Ḇ', + 'ḇ' => 'ḇ', + 'Ḉ' => 'Ḉ', + 'ḉ' => 'ḉ', + 'Ḋ' => 'Ḋ', + 'ḋ' => 'ḋ', + 'Ḍ' => 'Ḍ', + 'ḍ' => 'ḍ', + 'Ḏ' => 'Ḏ', + 'ḏ' => 'ḏ', + 'Ḑ' => 'Ḑ', + 'ḑ' => 'ḑ', + 'Ḓ' => 'Ḓ', + 'ḓ' => 'ḓ', + 'Ḕ' => 'Ḕ', + 'ḕ' => 'ḕ', + 'Ḗ' => 'Ḗ', + 'ḗ' => 'ḗ', + 'Ḙ' => 'Ḙ', + 'ḙ' => 'ḙ', + 'Ḛ' => 'Ḛ', + 'ḛ' => 'ḛ', + 'Ḝ' => 'Ḝ', + 'ḝ' => 'ḝ', + 'Ḟ' => 'Ḟ', + 'ḟ' => 'ḟ', + 'Ḡ' => 'Ḡ', + 'ḡ' => 'ḡ', + 'Ḣ' => 'Ḣ', + 'ḣ' => 'ḣ', + 'Ḥ' => 'Ḥ', + 'ḥ' => 'ḥ', + 'Ḧ' => 'Ḧ', + 'ḧ' => 'ḧ', + 'Ḩ' => 'Ḩ', + 'ḩ' => 'ḩ', + 'Ḫ' => 'Ḫ', + 'ḫ' => 'ḫ', + 'Ḭ' => 'Ḭ', + 'ḭ' => 'ḭ', + 'Ḯ' => 'Ḯ', + 'ḯ' => 'ḯ', + 'Ḱ' => 'Ḱ', + 'ḱ' => 'ḱ', + 'Ḳ' => 'Ḳ', + 'ḳ' => 'ḳ', + 'Ḵ' => 'Ḵ', + 'ḵ' => 'ḵ', + 'Ḷ' => 'Ḷ', + 'ḷ' => 'ḷ', + 'Ḹ' => 'Ḹ', + 'ḹ' => 'ḹ', + 'Ḻ' => 'Ḻ', + 'ḻ' => 'ḻ', + 'Ḽ' => 'Ḽ', + 'ḽ' => 'ḽ', + 'Ḿ' => 'Ḿ', + 'ḿ' => 'ḿ', + 'Ṁ' => 'Ṁ', + 'ṁ' => 'ṁ', + 'Ṃ' => 'Ṃ', + 'ṃ' => 'ṃ', + 'Ṅ' => 'Ṅ', + 'ṅ' => 'ṅ', + 'Ṇ' => 'Ṇ', + 'ṇ' => 'ṇ', + 'Ṉ' => 'Ṉ', + 'ṉ' => 'ṉ', + 'Ṋ' => 'Ṋ', + 'ṋ' => 'ṋ', + 'Ṍ' => 'Ṍ', + 'ṍ' => 'ṍ', + 'Ṏ' => 'Ṏ', + 'ṏ' => 'ṏ', + 'Ṑ' => 'Ṑ', + 'ṑ' => 'ṑ', + 'Ṓ' => 'Ṓ', + 'ṓ' => 'ṓ', + 'Ṕ' => 'Ṕ', + 'ṕ' => 'ṕ', + 'Ṗ' => 'Ṗ', + 'ṗ' => 'ṗ', + 'Ṙ' => 'Ṙ', + 'ṙ' => 'ṙ', + 'Ṛ' => 'Ṛ', + 'ṛ' => 'ṛ', + 'Ṝ' => 'Ṝ', + 'ṝ' => 'ṝ', + 'Ṟ' => 'Ṟ', + 'ṟ' => 'ṟ', + 'Ṡ' => 'Ṡ', + 'ṡ' => 'ṡ', + 'Ṣ' => 'Ṣ', + 'ṣ' => 'ṣ', + 'Ṥ' => 'Ṥ', + 'ṥ' => 'ṥ', + 'Ṧ' => 'Ṧ', + 'ṧ' => 'ṧ', + 'Ṩ' => 'Ṩ', + 'ṩ' => 'ṩ', + 'Ṫ' => 'Ṫ', + 'ṫ' => 'ṫ', + 'Ṭ' => 'Ṭ', + 'ṭ' => 'ṭ', + 'Ṯ' => 'Ṯ', + 'ṯ' => 'ṯ', + 'Ṱ' => 'Ṱ', + 'ṱ' => 'ṱ', + 'Ṳ' => 'Ṳ', + 'ṳ' => 'ṳ', + 'Ṵ' => 'Ṵ', + 'ṵ' => 'ṵ', + 'Ṷ' => 'Ṷ', + 'ṷ' => 'ṷ', + 'Ṹ' => 'Ṹ', + 'ṹ' => 'ṹ', + 'Ṻ' => 'Ṻ', + 'ṻ' => 'ṻ', + 'Ṽ' => 'Ṽ', + 'ṽ' => 'ṽ', + 'Ṿ' => 'Ṿ', + 'ṿ' => 'ṿ', + 'Ẁ' => 'Ẁ', + 'ẁ' => 'ẁ', + 'Ẃ' => 'Ẃ', + 'ẃ' => 'ẃ', + 'Ẅ' => 'Ẅ', + 'ẅ' => 'ẅ', + 'Ẇ' => 'Ẇ', + 'ẇ' => 'ẇ', + 'Ẉ' => 'Ẉ', + 'ẉ' => 'ẉ', + 'Ẋ' => 'Ẋ', + 'ẋ' => 'ẋ', + 'Ẍ' => 'Ẍ', + 'ẍ' => 'ẍ', + 'Ẏ' => 'Ẏ', + 'ẏ' => 'ẏ', + 'Ẑ' => 'Ẑ', + 'ẑ' => 'ẑ', + 'Ẓ' => 'Ẓ', + 'ẓ' => 'ẓ', + 'Ẕ' => 'Ẕ', + 'ẕ' => 'ẕ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẛ' => 'ẛ', + 'Ạ' => 'Ạ', + 'ạ' => 'ạ', + 'Ả' => 'Ả', + 'ả' => 'ả', + 'Ấ' => 'Ấ', + 'ấ' => 'ấ', + 'Ầ' => 'Ầ', + 'ầ' => 'ầ', + 'Ẩ' => 'Ẩ', + 'ẩ' => 'ẩ', + 'Ẫ' => 'Ẫ', + 'ẫ' => 'ẫ', + 'Ậ' => 'Ậ', + 'ậ' => 'ậ', + 'Ắ' => 'Ắ', + 'ắ' => 'ắ', + 'Ằ' => 'Ằ', + 'ằ' => 'ằ', + 'Ẳ' => 'Ẳ', + 'ẳ' => 'ẳ', + 'Ẵ' => 'Ẵ', + 'ẵ' => 'ẵ', + 'Ặ' => 'Ặ', + 'ặ' => 'ặ', + 'Ẹ' => 'Ẹ', + 'ẹ' => 'ẹ', + 'Ẻ' => 'Ẻ', + 'ẻ' => 'ẻ', + 'Ẽ' => 'Ẽ', + 'ẽ' => 'ẽ', + 'Ế' => 'Ế', + 'ế' => 'ế', + 'Ề' => 'Ề', + 'ề' => 'ề', + 'Ể' => 'Ể', + 'ể' => 'ể', + 'Ễ' => 'Ễ', + 'ễ' => 'ễ', + 'Ệ' => 'Ệ', + 'ệ' => 'ệ', + 'Ỉ' => 'Ỉ', + 'ỉ' => 'ỉ', + 'Ị' => 'Ị', + 'ị' => 'ị', + 'Ọ' => 'Ọ', + 'ọ' => 'ọ', + 'Ỏ' => 'Ỏ', + 'ỏ' => 'ỏ', + 'Ố' => 'Ố', + 'ố' => 'ố', + 'Ồ' => 'Ồ', + 'ồ' => 'ồ', + 'Ổ' => 'Ổ', + 'ổ' => 'ổ', + 'Ỗ' => 'Ỗ', + 'ỗ' => 'ỗ', + 'Ộ' => 'Ộ', + 'ộ' => 'ộ', + 'Ớ' => 'Ớ', + 'ớ' => 'ớ', + 'Ờ' => 'Ờ', + 'ờ' => 'ờ', + 'Ở' => 'Ở', + 'ở' => 'ở', + 'Ỡ' => 'Ỡ', + 'ỡ' => 'ỡ', + 'Ợ' => 'Ợ', + 'ợ' => 'ợ', + 'Ụ' => 'Ụ', + 'ụ' => 'ụ', + 'Ủ' => 'Ủ', + 'ủ' => 'ủ', + 'Ứ' => 'Ứ', + 'ứ' => 'ứ', + 'Ừ' => 'Ừ', + 'ừ' => 'ừ', + 'Ử' => 'Ử', + 'ử' => 'ử', + 'Ữ' => 'Ữ', + 'ữ' => 'ữ', + 'Ự' => 'Ự', + 'ự' => 'ự', + 'Ỳ' => 'Ỳ', + 'ỳ' => 'ỳ', + 'Ỵ' => 'Ỵ', + 'ỵ' => 'ỵ', + 'Ỷ' => 'Ỷ', + 'ỷ' => 'ỷ', + 'Ỹ' => 'Ỹ', + 'ỹ' => 'ỹ', + 'ἀ' => 'ἀ', + 'ἁ' => 'ἁ', + 'ἂ' => 'ἂ', + 'ἃ' => 'ἃ', + 'ἄ' => 'ἄ', + 'ἅ' => 'ἅ', + 'ἆ' => 'ἆ', + 'ἇ' => 'ἇ', + 'Ἀ' => 'Ἀ', + 'Ἁ' => 'Ἁ', + 'Ἂ' => 'Ἂ', + 'Ἃ' => 'Ἃ', + 'Ἄ' => 'Ἄ', + 'Ἅ' => 'Ἅ', + 'Ἆ' => 'Ἆ', + 'Ἇ' => 'Ἇ', + 'ἐ' => 'ἐ', + 'ἑ' => 'ἑ', + 'ἒ' => 'ἒ', + 'ἓ' => 'ἓ', + 'ἔ' => 'ἔ', + 'ἕ' => 'ἕ', + 'Ἐ' => 'Ἐ', + 'Ἑ' => 'Ἑ', + 'Ἒ' => 'Ἒ', + 'Ἓ' => 'Ἓ', + 'Ἔ' => 'Ἔ', + 'Ἕ' => 'Ἕ', + 'ἠ' => 'ἠ', + 'ἡ' => 'ἡ', + 'ἢ' => 'ἢ', + 'ἣ' => 'ἣ', + 'ἤ' => 'ἤ', + 'ἥ' => 'ἥ', + 'ἦ' => 'ἦ', + 'ἧ' => 'ἧ', + 'Ἠ' => 'Ἠ', + 'Ἡ' => 'Ἡ', + 'Ἢ' => 'Ἢ', + 'Ἣ' => 'Ἣ', + 'Ἤ' => 'Ἤ', + 'Ἥ' => 'Ἥ', + 'Ἦ' => 'Ἦ', + 'Ἧ' => 'Ἧ', + 'ἰ' => 'ἰ', + 'ἱ' => 'ἱ', + 'ἲ' => 'ἲ', + 'ἳ' => 'ἳ', + 'ἴ' => 'ἴ', + 'ἵ' => 'ἵ', + 'ἶ' => 'ἶ', + 'ἷ' => 'ἷ', + 'Ἰ' => 'Ἰ', + 'Ἱ' => 'Ἱ', + 'Ἲ' => 'Ἲ', + 'Ἳ' => 'Ἳ', + 'Ἴ' => 'Ἴ', + 'Ἵ' => 'Ἵ', + 'Ἶ' => 'Ἶ', + 'Ἷ' => 'Ἷ', + 'ὀ' => 'ὀ', + 'ὁ' => 'ὁ', + 'ὂ' => 'ὂ', + 'ὃ' => 'ὃ', + 'ὄ' => 'ὄ', + 'ὅ' => 'ὅ', + 'Ὀ' => 'Ὀ', + 'Ὁ' => 'Ὁ', + 'Ὂ' => 'Ὂ', + 'Ὃ' => 'Ὃ', + 'Ὄ' => 'Ὄ', + 'Ὅ' => 'Ὅ', + 'ὐ' => 'ὐ', + 'ὑ' => 'ὑ', + 'ὒ' => 'ὒ', + 'ὓ' => 'ὓ', + 'ὔ' => 'ὔ', + 'ὕ' => 'ὕ', + 'ὖ' => 'ὖ', + 'ὗ' => 'ὗ', + 'Ὑ' => 'Ὑ', + 'Ὓ' => 'Ὓ', + 'Ὕ' => 'Ὕ', + 'Ὗ' => 'Ὗ', + 'ὠ' => 'ὠ', + 'ὡ' => 'ὡ', + 'ὢ' => 'ὢ', + 'ὣ' => 'ὣ', + 'ὤ' => 'ὤ', + 'ὥ' => 'ὥ', + 'ὦ' => 'ὦ', + 'ὧ' => 'ὧ', + 'Ὠ' => 'Ὠ', + 'Ὡ' => 'Ὡ', + 'Ὢ' => 'Ὢ', + 'Ὣ' => 'Ὣ', + 'Ὤ' => 'Ὤ', + 'Ὥ' => 'Ὥ', + 'Ὦ' => 'Ὦ', + 'Ὧ' => 'Ὧ', + 'ὰ' => 'ὰ', + 'ὲ' => 'ὲ', + 'ὴ' => 'ὴ', + 'ὶ' => 'ὶ', + 'ὸ' => 'ὸ', + 'ὺ' => 'ὺ', + 'ὼ' => 'ὼ', + 'ᾀ' => 'ᾀ', + 'ᾁ' => 'ᾁ', + 'ᾂ' => 'ᾂ', + 'ᾃ' => 'ᾃ', + 'ᾄ' => 'ᾄ', + 'ᾅ' => 'ᾅ', + 'ᾆ' => 'ᾆ', + 'ᾇ' => 'ᾇ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾐ', + 'ᾑ' => 'ᾑ', + 'ᾒ' => 'ᾒ', + 'ᾓ' => 'ᾓ', + 'ᾔ' => 'ᾔ', + 'ᾕ' => 'ᾕ', + 'ᾖ' => 'ᾖ', + 'ᾗ' => 'ᾗ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾠ', + 'ᾡ' => 'ᾡ', + 'ᾢ' => 'ᾢ', + 'ᾣ' => 'ᾣ', + 'ᾤ' => 'ᾤ', + 'ᾥ' => 'ᾥ', + 'ᾦ' => 'ᾦ', + 'ᾧ' => 'ᾧ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾰ' => 'ᾰ', + 'ᾱ' => 'ᾱ', + 'ᾲ' => 'ᾲ', + 'ᾳ' => 'ᾳ', + 'ᾴ' => 'ᾴ', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾷ', + 'Ᾰ' => 'Ᾰ', + 'Ᾱ' => 'Ᾱ', + 'Ὰ' => 'Ὰ', + 'ᾼ' => 'ᾼ', + '῁' => '῁', + 'ῂ' => 'ῂ', + 'ῃ' => 'ῃ', + 'ῄ' => 'ῄ', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῇ', + 'Ὲ' => 'Ὲ', + 'Ὴ' => 'Ὴ', + 'ῌ' => 'ῌ', + '῍' => '῍', + '῎' => '῎', + '῏' => '῏', + 'ῐ' => 'ῐ', + 'ῑ' => 'ῑ', + 'ῒ' => 'ῒ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'Ῐ' => 'Ῐ', + 'Ῑ' => 'Ῑ', + 'Ὶ' => 'Ὶ', + '῝' => '῝', + '῞' => '῞', + '῟' => '῟', + 'ῠ' => 'ῠ', + 'ῡ' => 'ῡ', + 'ῢ' => 'ῢ', + 'ῤ' => 'ῤ', + 'ῥ' => 'ῥ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'Ῠ' => 'Ῠ', + 'Ῡ' => 'Ῡ', + 'Ὺ' => 'Ὺ', + 'Ῥ' => 'Ῥ', + '῭' => '῭', + 'ῲ' => 'ῲ', + 'ῳ' => 'ῳ', + 'ῴ' => 'ῴ', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῷ', + 'Ὸ' => 'Ὸ', + 'Ὼ' => 'Ὼ', + 'ῼ' => 'ῼ', + '↚' => '↚', + '↛' => '↛', + '↮' => '↮', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '∄' => '∄', + '∉' => '∉', + '∌' => '∌', + '∤' => '∤', + '∦' => '∦', + '≁' => '≁', + '≄' => '≄', + '≇' => '≇', + '≉' => '≉', + '≠' => '≠', + '≢' => '≢', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≴' => '≴', + '≵' => '≵', + '≸' => '≸', + '≹' => '≹', + '⊀' => '⊀', + '⊁' => '⊁', + '⊄' => '⊄', + '⊅' => '⊅', + '⊈' => '⊈', + '⊉' => '⊉', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + 'が' => 'が', + 'ぎ' => 'ぎ', + 'ぐ' => 'ぐ', + 'げ' => 'げ', + 'ご' => 'ご', + 'ざ' => 'ざ', + 'じ' => 'じ', + 'ず' => 'ず', + 'ぜ' => 'ぜ', + 'ぞ' => 'ぞ', + 'だ' => 'だ', + 'ぢ' => 'ぢ', + 'づ' => 'づ', + 'で' => 'で', + 'ど' => 'ど', + 'ば' => 'ば', + 'ぱ' => 'ぱ', + 'び' => 'び', + 'ぴ' => 'ぴ', + 'ぶ' => 'ぶ', + 'ぷ' => 'ぷ', + 'べ' => 'べ', + 'ぺ' => 'ぺ', + 'ぼ' => 'ぼ', + 'ぽ' => 'ぽ', + 'ゔ' => 'ゔ', + 'ゞ' => 'ゞ', + 'ガ' => 'ガ', + 'ギ' => 'ギ', + 'グ' => 'グ', + 'ゲ' => 'ゲ', + 'ゴ' => 'ゴ', + 'ザ' => 'ザ', + 'ジ' => 'ジ', + 'ズ' => 'ズ', + 'ゼ' => 'ゼ', + 'ゾ' => 'ゾ', + 'ダ' => 'ダ', + 'ヂ' => 'ヂ', + 'ヅ' => 'ヅ', + 'デ' => 'デ', + 'ド' => 'ド', + 'バ' => 'バ', + 'パ' => 'パ', + 'ビ' => 'ビ', + 'ピ' => 'ピ', + 'ブ' => 'ブ', + 'プ' => 'プ', + 'ベ' => 'ベ', + 'ペ' => 'ペ', + 'ボ' => 'ボ', + 'ポ' => 'ポ', + 'ヴ' => 'ヴ', + 'ヷ' => 'ヷ', + 'ヸ' => 'ヸ', + 'ヹ' => 'ヹ', + 'ヺ' => 'ヺ', + 'ヾ' => 'ヾ', + '𑂚' => '𑂚', + '𑂜' => '𑂜', + '𑂫' => '𑂫', + '𑄮' => '𑄮', + '𑄯' => '𑄯', + '𑍋' => '𑍋', + '𑍌' => '𑍌', + '𑒻' => '𑒻', + '𑒼' => '𑒼', + '𑒾' => '𑒾', + '𑖺' => '𑖺', + '𑖻' => '𑖻', + '𑤸' => '𑤸', +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php new file mode 100644 index 0000000..5a3e8e0 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php @@ -0,0 +1,2065 @@ + 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ĕ' => 'Ĕ', + 'ĕ' => 'ĕ', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'ģ' => 'ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Ĭ' => 'Ĭ', + 'ĭ' => 'ĭ', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ŏ' => 'Ŏ', + 'ŏ' => 'ŏ', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'Ơ' => 'Ơ', + 'ơ' => 'ơ', + 'Ư' => 'Ư', + 'ư' => 'ư', + 'Ǎ' => 'Ǎ', + 'ǎ' => 'ǎ', + 'Ǐ' => 'Ǐ', + 'ǐ' => 'ǐ', + 'Ǒ' => 'Ǒ', + 'ǒ' => 'ǒ', + 'Ǔ' => 'Ǔ', + 'ǔ' => 'ǔ', + 'Ǖ' => 'Ǖ', + 'ǖ' => 'ǖ', + 'Ǘ' => 'Ǘ', + 'ǘ' => 'ǘ', + 'Ǚ' => 'Ǚ', + 'ǚ' => 'ǚ', + 'Ǜ' => 'Ǜ', + 'ǜ' => 'ǜ', + 'Ǟ' => 'Ǟ', + 'ǟ' => 'ǟ', + 'Ǡ' => 'Ǡ', + 'ǡ' => 'ǡ', + 'Ǣ' => 'Ǣ', + 'ǣ' => 'ǣ', + 'Ǧ' => 'Ǧ', + 'ǧ' => 'ǧ', + 'Ǩ' => 'Ǩ', + 'ǩ' => 'ǩ', + 'Ǫ' => 'Ǫ', + 'ǫ' => 'ǫ', + 'Ǭ' => 'Ǭ', + 'ǭ' => 'ǭ', + 'Ǯ' => 'Ǯ', + 'ǯ' => 'ǯ', + 'ǰ' => 'ǰ', + 'Ǵ' => 'Ǵ', + 'ǵ' => 'ǵ', + 'Ǹ' => 'Ǹ', + 'ǹ' => 'ǹ', + 'Ǻ' => 'Ǻ', + 'ǻ' => 'ǻ', + 'Ǽ' => 'Ǽ', + 'ǽ' => 'ǽ', + 'Ǿ' => 'Ǿ', + 'ǿ' => 'ǿ', + 'Ȁ' => 'Ȁ', + 'ȁ' => 'ȁ', + 'Ȃ' => 'Ȃ', + 'ȃ' => 'ȃ', + 'Ȅ' => 'Ȅ', + 'ȅ' => 'ȅ', + 'Ȇ' => 'Ȇ', + 'ȇ' => 'ȇ', + 'Ȉ' => 'Ȉ', + 'ȉ' => 'ȉ', + 'Ȋ' => 'Ȋ', + 'ȋ' => 'ȋ', + 'Ȍ' => 'Ȍ', + 'ȍ' => 'ȍ', + 'Ȏ' => 'Ȏ', + 'ȏ' => 'ȏ', + 'Ȑ' => 'Ȑ', + 'ȑ' => 'ȑ', + 'Ȓ' => 'Ȓ', + 'ȓ' => 'ȓ', + 'Ȕ' => 'Ȕ', + 'ȕ' => 'ȕ', + 'Ȗ' => 'Ȗ', + 'ȗ' => 'ȗ', + 'Ș' => 'Ș', + 'ș' => 'ș', + 'Ț' => 'Ț', + 'ț' => 'ț', + 'Ȟ' => 'Ȟ', + 'ȟ' => 'ȟ', + 'Ȧ' => 'Ȧ', + 'ȧ' => 'ȧ', + 'Ȩ' => 'Ȩ', + 'ȩ' => 'ȩ', + 'Ȫ' => 'Ȫ', + 'ȫ' => 'ȫ', + 'Ȭ' => 'Ȭ', + 'ȭ' => 'ȭ', + 'Ȯ' => 'Ȯ', + 'ȯ' => 'ȯ', + 'Ȱ' => 'Ȱ', + 'ȱ' => 'ȱ', + 'Ȳ' => 'Ȳ', + 'ȳ' => 'ȳ', + '̀' => '̀', + '́' => '́', + '̓' => '̓', + '̈́' => '̈́', + 'ʹ' => 'ʹ', + ';' => ';', + '΅' => '΅', + 'Ά' => 'Ά', + '·' => '·', + 'Έ' => 'Έ', + 'Ή' => 'Ή', + 'Ί' => 'Ί', + 'Ό' => 'Ό', + 'Ύ' => 'Ύ', + 'Ώ' => 'Ώ', + 'ΐ' => 'ΐ', + 'Ϊ' => 'Ϊ', + 'Ϋ' => 'Ϋ', + 'ά' => 'ά', + 'έ' => 'έ', + 'ή' => 'ή', + 'ί' => 'ί', + 'ΰ' => 'ΰ', + 'ϊ' => 'ϊ', + 'ϋ' => 'ϋ', + 'ό' => 'ό', + 'ύ' => 'ύ', + 'ώ' => 'ώ', + 'ϓ' => 'ϓ', + 'ϔ' => 'ϔ', + 'Ѐ' => 'Ѐ', + 'Ё' => 'Ё', + 'Ѓ' => 'Ѓ', + 'Ї' => 'Ї', + 'Ќ' => 'Ќ', + 'Ѝ' => 'Ѝ', + 'Ў' => 'Ў', + 'Й' => 'Й', + 'й' => 'й', + 'ѐ' => 'ѐ', + 'ё' => 'ё', + 'ѓ' => 'ѓ', + 'ї' => 'ї', + 'ќ' => 'ќ', + 'ѝ' => 'ѝ', + 'ў' => 'ў', + 'Ѷ' => 'Ѷ', + 'ѷ' => 'ѷ', + 'Ӂ' => 'Ӂ', + 'ӂ' => 'ӂ', + 'Ӑ' => 'Ӑ', + 'ӑ' => 'ӑ', + 'Ӓ' => 'Ӓ', + 'ӓ' => 'ӓ', + 'Ӗ' => 'Ӗ', + 'ӗ' => 'ӗ', + 'Ӛ' => 'Ӛ', + 'ӛ' => 'ӛ', + 'Ӝ' => 'Ӝ', + 'ӝ' => 'ӝ', + 'Ӟ' => 'Ӟ', + 'ӟ' => 'ӟ', + 'Ӣ' => 'Ӣ', + 'ӣ' => 'ӣ', + 'Ӥ' => 'Ӥ', + 'ӥ' => 'ӥ', + 'Ӧ' => 'Ӧ', + 'ӧ' => 'ӧ', + 'Ӫ' => 'Ӫ', + 'ӫ' => 'ӫ', + 'Ӭ' => 'Ӭ', + 'ӭ' => 'ӭ', + 'Ӯ' => 'Ӯ', + 'ӯ' => 'ӯ', + 'Ӱ' => 'Ӱ', + 'ӱ' => 'ӱ', + 'Ӳ' => 'Ӳ', + 'ӳ' => 'ӳ', + 'Ӵ' => 'Ӵ', + 'ӵ' => 'ӵ', + 'Ӹ' => 'Ӹ', + 'ӹ' => 'ӹ', + 'آ' => 'آ', + 'أ' => 'أ', + 'ؤ' => 'ؤ', + 'إ' => 'إ', + 'ئ' => 'ئ', + 'ۀ' => 'ۀ', + 'ۂ' => 'ۂ', + 'ۓ' => 'ۓ', + 'ऩ' => 'ऩ', + 'ऱ' => 'ऱ', + 'ऴ' => 'ऴ', + 'क़' => 'क़', + 'ख़' => 'ख़', + 'ग़' => 'ग़', + 'ज़' => 'ज़', + 'ड़' => 'ड़', + 'ढ़' => 'ढ़', + 'फ़' => 'फ़', + 'य़' => 'य़', + 'ো' => 'ো', + 'ৌ' => 'ৌ', + 'ড়' => 'ড়', + 'ঢ়' => 'ঢ়', + 'য়' => 'য়', + 'ਲ਼' => 'ਲ਼', + 'ਸ਼' => 'ਸ਼', + 'ਖ਼' => 'ਖ਼', + 'ਗ਼' => 'ਗ਼', + 'ਜ਼' => 'ਜ਼', + 'ਫ਼' => 'ਫ਼', + 'ୈ' => 'ୈ', + 'ୋ' => 'ୋ', + 'ୌ' => 'ୌ', + 'ଡ଼' => 'ଡ଼', + 'ଢ଼' => 'ଢ଼', + 'ஔ' => 'ஔ', + 'ொ' => 'ொ', + 'ோ' => 'ோ', + 'ௌ' => 'ௌ', + 'ై' => 'ై', + 'ೀ' => 'ೀ', + 'ೇ' => 'ೇ', + 'ೈ' => 'ೈ', + 'ೊ' => 'ೊ', + 'ೋ' => 'ೋ', + 'ൊ' => 'ൊ', + 'ോ' => 'ോ', + 'ൌ' => 'ൌ', + 'ේ' => 'ේ', + 'ො' => 'ො', + 'ෝ' => 'ෝ', + 'ෞ' => 'ෞ', + 'གྷ' => 'གྷ', + 'ཌྷ' => 'ཌྷ', + 'དྷ' => 'དྷ', + 'བྷ' => 'བྷ', + 'ཛྷ' => 'ཛྷ', + 'ཀྵ' => 'ཀྵ', + 'ཱི' => 'ཱི', + 'ཱུ' => 'ཱུ', + 'ྲྀ' => 'ྲྀ', + 'ླྀ' => 'ླྀ', + 'ཱྀ' => 'ཱྀ', + 'ྒྷ' => 'ྒྷ', + 'ྜྷ' => 'ྜྷ', + 'ྡྷ' => 'ྡྷ', + 'ྦྷ' => 'ྦྷ', + 'ྫྷ' => 'ྫྷ', + 'ྐྵ' => 'ྐྵ', + 'ဦ' => 'ဦ', + 'ᬆ' => 'ᬆ', + 'ᬈ' => 'ᬈ', + 'ᬊ' => 'ᬊ', + 'ᬌ' => 'ᬌ', + 'ᬎ' => 'ᬎ', + 'ᬒ' => 'ᬒ', + 'ᬻ' => 'ᬻ', + 'ᬽ' => 'ᬽ', + 'ᭀ' => 'ᭀ', + 'ᭁ' => 'ᭁ', + 'ᭃ' => 'ᭃ', + 'Ḁ' => 'Ḁ', + 'ḁ' => 'ḁ', + 'Ḃ' => 'Ḃ', + 'ḃ' => 'ḃ', + 'Ḅ' => 'Ḅ', + 'ḅ' => 'ḅ', + 'Ḇ' => 'Ḇ', + 'ḇ' => 'ḇ', + 'Ḉ' => 'Ḉ', + 'ḉ' => 'ḉ', + 'Ḋ' => 'Ḋ', + 'ḋ' => 'ḋ', + 'Ḍ' => 'Ḍ', + 'ḍ' => 'ḍ', + 'Ḏ' => 'Ḏ', + 'ḏ' => 'ḏ', + 'Ḑ' => 'Ḑ', + 'ḑ' => 'ḑ', + 'Ḓ' => 'Ḓ', + 'ḓ' => 'ḓ', + 'Ḕ' => 'Ḕ', + 'ḕ' => 'ḕ', + 'Ḗ' => 'Ḗ', + 'ḗ' => 'ḗ', + 'Ḙ' => 'Ḙ', + 'ḙ' => 'ḙ', + 'Ḛ' => 'Ḛ', + 'ḛ' => 'ḛ', + 'Ḝ' => 'Ḝ', + 'ḝ' => 'ḝ', + 'Ḟ' => 'Ḟ', + 'ḟ' => 'ḟ', + 'Ḡ' => 'Ḡ', + 'ḡ' => 'ḡ', + 'Ḣ' => 'Ḣ', + 'ḣ' => 'ḣ', + 'Ḥ' => 'Ḥ', + 'ḥ' => 'ḥ', + 'Ḧ' => 'Ḧ', + 'ḧ' => 'ḧ', + 'Ḩ' => 'Ḩ', + 'ḩ' => 'ḩ', + 'Ḫ' => 'Ḫ', + 'ḫ' => 'ḫ', + 'Ḭ' => 'Ḭ', + 'ḭ' => 'ḭ', + 'Ḯ' => 'Ḯ', + 'ḯ' => 'ḯ', + 'Ḱ' => 'Ḱ', + 'ḱ' => 'ḱ', + 'Ḳ' => 'Ḳ', + 'ḳ' => 'ḳ', + 'Ḵ' => 'Ḵ', + 'ḵ' => 'ḵ', + 'Ḷ' => 'Ḷ', + 'ḷ' => 'ḷ', + 'Ḹ' => 'Ḹ', + 'ḹ' => 'ḹ', + 'Ḻ' => 'Ḻ', + 'ḻ' => 'ḻ', + 'Ḽ' => 'Ḽ', + 'ḽ' => 'ḽ', + 'Ḿ' => 'Ḿ', + 'ḿ' => 'ḿ', + 'Ṁ' => 'Ṁ', + 'ṁ' => 'ṁ', + 'Ṃ' => 'Ṃ', + 'ṃ' => 'ṃ', + 'Ṅ' => 'Ṅ', + 'ṅ' => 'ṅ', + 'Ṇ' => 'Ṇ', + 'ṇ' => 'ṇ', + 'Ṉ' => 'Ṉ', + 'ṉ' => 'ṉ', + 'Ṋ' => 'Ṋ', + 'ṋ' => 'ṋ', + 'Ṍ' => 'Ṍ', + 'ṍ' => 'ṍ', + 'Ṏ' => 'Ṏ', + 'ṏ' => 'ṏ', + 'Ṑ' => 'Ṑ', + 'ṑ' => 'ṑ', + 'Ṓ' => 'Ṓ', + 'ṓ' => 'ṓ', + 'Ṕ' => 'Ṕ', + 'ṕ' => 'ṕ', + 'Ṗ' => 'Ṗ', + 'ṗ' => 'ṗ', + 'Ṙ' => 'Ṙ', + 'ṙ' => 'ṙ', + 'Ṛ' => 'Ṛ', + 'ṛ' => 'ṛ', + 'Ṝ' => 'Ṝ', + 'ṝ' => 'ṝ', + 'Ṟ' => 'Ṟ', + 'ṟ' => 'ṟ', + 'Ṡ' => 'Ṡ', + 'ṡ' => 'ṡ', + 'Ṣ' => 'Ṣ', + 'ṣ' => 'ṣ', + 'Ṥ' => 'Ṥ', + 'ṥ' => 'ṥ', + 'Ṧ' => 'Ṧ', + 'ṧ' => 'ṧ', + 'Ṩ' => 'Ṩ', + 'ṩ' => 'ṩ', + 'Ṫ' => 'Ṫ', + 'ṫ' => 'ṫ', + 'Ṭ' => 'Ṭ', + 'ṭ' => 'ṭ', + 'Ṯ' => 'Ṯ', + 'ṯ' => 'ṯ', + 'Ṱ' => 'Ṱ', + 'ṱ' => 'ṱ', + 'Ṳ' => 'Ṳ', + 'ṳ' => 'ṳ', + 'Ṵ' => 'Ṵ', + 'ṵ' => 'ṵ', + 'Ṷ' => 'Ṷ', + 'ṷ' => 'ṷ', + 'Ṹ' => 'Ṹ', + 'ṹ' => 'ṹ', + 'Ṻ' => 'Ṻ', + 'ṻ' => 'ṻ', + 'Ṽ' => 'Ṽ', + 'ṽ' => 'ṽ', + 'Ṿ' => 'Ṿ', + 'ṿ' => 'ṿ', + 'Ẁ' => 'Ẁ', + 'ẁ' => 'ẁ', + 'Ẃ' => 'Ẃ', + 'ẃ' => 'ẃ', + 'Ẅ' => 'Ẅ', + 'ẅ' => 'ẅ', + 'Ẇ' => 'Ẇ', + 'ẇ' => 'ẇ', + 'Ẉ' => 'Ẉ', + 'ẉ' => 'ẉ', + 'Ẋ' => 'Ẋ', + 'ẋ' => 'ẋ', + 'Ẍ' => 'Ẍ', + 'ẍ' => 'ẍ', + 'Ẏ' => 'Ẏ', + 'ẏ' => 'ẏ', + 'Ẑ' => 'Ẑ', + 'ẑ' => 'ẑ', + 'Ẓ' => 'Ẓ', + 'ẓ' => 'ẓ', + 'Ẕ' => 'Ẕ', + 'ẕ' => 'ẕ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẛ' => 'ẛ', + 'Ạ' => 'Ạ', + 'ạ' => 'ạ', + 'Ả' => 'Ả', + 'ả' => 'ả', + 'Ấ' => 'Ấ', + 'ấ' => 'ấ', + 'Ầ' => 'Ầ', + 'ầ' => 'ầ', + 'Ẩ' => 'Ẩ', + 'ẩ' => 'ẩ', + 'Ẫ' => 'Ẫ', + 'ẫ' => 'ẫ', + 'Ậ' => 'Ậ', + 'ậ' => 'ậ', + 'Ắ' => 'Ắ', + 'ắ' => 'ắ', + 'Ằ' => 'Ằ', + 'ằ' => 'ằ', + 'Ẳ' => 'Ẳ', + 'ẳ' => 'ẳ', + 'Ẵ' => 'Ẵ', + 'ẵ' => 'ẵ', + 'Ặ' => 'Ặ', + 'ặ' => 'ặ', + 'Ẹ' => 'Ẹ', + 'ẹ' => 'ẹ', + 'Ẻ' => 'Ẻ', + 'ẻ' => 'ẻ', + 'Ẽ' => 'Ẽ', + 'ẽ' => 'ẽ', + 'Ế' => 'Ế', + 'ế' => 'ế', + 'Ề' => 'Ề', + 'ề' => 'ề', + 'Ể' => 'Ể', + 'ể' => 'ể', + 'Ễ' => 'Ễ', + 'ễ' => 'ễ', + 'Ệ' => 'Ệ', + 'ệ' => 'ệ', + 'Ỉ' => 'Ỉ', + 'ỉ' => 'ỉ', + 'Ị' => 'Ị', + 'ị' => 'ị', + 'Ọ' => 'Ọ', + 'ọ' => 'ọ', + 'Ỏ' => 'Ỏ', + 'ỏ' => 'ỏ', + 'Ố' => 'Ố', + 'ố' => 'ố', + 'Ồ' => 'Ồ', + 'ồ' => 'ồ', + 'Ổ' => 'Ổ', + 'ổ' => 'ổ', + 'Ỗ' => 'Ỗ', + 'ỗ' => 'ỗ', + 'Ộ' => 'Ộ', + 'ộ' => 'ộ', + 'Ớ' => 'Ớ', + 'ớ' => 'ớ', + 'Ờ' => 'Ờ', + 'ờ' => 'ờ', + 'Ở' => 'Ở', + 'ở' => 'ở', + 'Ỡ' => 'Ỡ', + 'ỡ' => 'ỡ', + 'Ợ' => 'Ợ', + 'ợ' => 'ợ', + 'Ụ' => 'Ụ', + 'ụ' => 'ụ', + 'Ủ' => 'Ủ', + 'ủ' => 'ủ', + 'Ứ' => 'Ứ', + 'ứ' => 'ứ', + 'Ừ' => 'Ừ', + 'ừ' => 'ừ', + 'Ử' => 'Ử', + 'ử' => 'ử', + 'Ữ' => 'Ữ', + 'ữ' => 'ữ', + 'Ự' => 'Ự', + 'ự' => 'ự', + 'Ỳ' => 'Ỳ', + 'ỳ' => 'ỳ', + 'Ỵ' => 'Ỵ', + 'ỵ' => 'ỵ', + 'Ỷ' => 'Ỷ', + 'ỷ' => 'ỷ', + 'Ỹ' => 'Ỹ', + 'ỹ' => 'ỹ', + 'ἀ' => 'ἀ', + 'ἁ' => 'ἁ', + 'ἂ' => 'ἂ', + 'ἃ' => 'ἃ', + 'ἄ' => 'ἄ', + 'ἅ' => 'ἅ', + 'ἆ' => 'ἆ', + 'ἇ' => 'ἇ', + 'Ἀ' => 'Ἀ', + 'Ἁ' => 'Ἁ', + 'Ἂ' => 'Ἂ', + 'Ἃ' => 'Ἃ', + 'Ἄ' => 'Ἄ', + 'Ἅ' => 'Ἅ', + 'Ἆ' => 'Ἆ', + 'Ἇ' => 'Ἇ', + 'ἐ' => 'ἐ', + 'ἑ' => 'ἑ', + 'ἒ' => 'ἒ', + 'ἓ' => 'ἓ', + 'ἔ' => 'ἔ', + 'ἕ' => 'ἕ', + 'Ἐ' => 'Ἐ', + 'Ἑ' => 'Ἑ', + 'Ἒ' => 'Ἒ', + 'Ἓ' => 'Ἓ', + 'Ἔ' => 'Ἔ', + 'Ἕ' => 'Ἕ', + 'ἠ' => 'ἠ', + 'ἡ' => 'ἡ', + 'ἢ' => 'ἢ', + 'ἣ' => 'ἣ', + 'ἤ' => 'ἤ', + 'ἥ' => 'ἥ', + 'ἦ' => 'ἦ', + 'ἧ' => 'ἧ', + 'Ἠ' => 'Ἠ', + 'Ἡ' => 'Ἡ', + 'Ἢ' => 'Ἢ', + 'Ἣ' => 'Ἣ', + 'Ἤ' => 'Ἤ', + 'Ἥ' => 'Ἥ', + 'Ἦ' => 'Ἦ', + 'Ἧ' => 'Ἧ', + 'ἰ' => 'ἰ', + 'ἱ' => 'ἱ', + 'ἲ' => 'ἲ', + 'ἳ' => 'ἳ', + 'ἴ' => 'ἴ', + 'ἵ' => 'ἵ', + 'ἶ' => 'ἶ', + 'ἷ' => 'ἷ', + 'Ἰ' => 'Ἰ', + 'Ἱ' => 'Ἱ', + 'Ἲ' => 'Ἲ', + 'Ἳ' => 'Ἳ', + 'Ἴ' => 'Ἴ', + 'Ἵ' => 'Ἵ', + 'Ἶ' => 'Ἶ', + 'Ἷ' => 'Ἷ', + 'ὀ' => 'ὀ', + 'ὁ' => 'ὁ', + 'ὂ' => 'ὂ', + 'ὃ' => 'ὃ', + 'ὄ' => 'ὄ', + 'ὅ' => 'ὅ', + 'Ὀ' => 'Ὀ', + 'Ὁ' => 'Ὁ', + 'Ὂ' => 'Ὂ', + 'Ὃ' => 'Ὃ', + 'Ὄ' => 'Ὄ', + 'Ὅ' => 'Ὅ', + 'ὐ' => 'ὐ', + 'ὑ' => 'ὑ', + 'ὒ' => 'ὒ', + 'ὓ' => 'ὓ', + 'ὔ' => 'ὔ', + 'ὕ' => 'ὕ', + 'ὖ' => 'ὖ', + 'ὗ' => 'ὗ', + 'Ὑ' => 'Ὑ', + 'Ὓ' => 'Ὓ', + 'Ὕ' => 'Ὕ', + 'Ὗ' => 'Ὗ', + 'ὠ' => 'ὠ', + 'ὡ' => 'ὡ', + 'ὢ' => 'ὢ', + 'ὣ' => 'ὣ', + 'ὤ' => 'ὤ', + 'ὥ' => 'ὥ', + 'ὦ' => 'ὦ', + 'ὧ' => 'ὧ', + 'Ὠ' => 'Ὠ', + 'Ὡ' => 'Ὡ', + 'Ὢ' => 'Ὢ', + 'Ὣ' => 'Ὣ', + 'Ὤ' => 'Ὤ', + 'Ὥ' => 'Ὥ', + 'Ὦ' => 'Ὦ', + 'Ὧ' => 'Ὧ', + 'ὰ' => 'ὰ', + 'ά' => 'ά', + 'ὲ' => 'ὲ', + 'έ' => 'έ', + 'ὴ' => 'ὴ', + 'ή' => 'ή', + 'ὶ' => 'ὶ', + 'ί' => 'ί', + 'ὸ' => 'ὸ', + 'ό' => 'ό', + 'ὺ' => 'ὺ', + 'ύ' => 'ύ', + 'ὼ' => 'ὼ', + 'ώ' => 'ώ', + 'ᾀ' => 'ᾀ', + 'ᾁ' => 'ᾁ', + 'ᾂ' => 'ᾂ', + 'ᾃ' => 'ᾃ', + 'ᾄ' => 'ᾄ', + 'ᾅ' => 'ᾅ', + 'ᾆ' => 'ᾆ', + 'ᾇ' => 'ᾇ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾐ', + 'ᾑ' => 'ᾑ', + 'ᾒ' => 'ᾒ', + 'ᾓ' => 'ᾓ', + 'ᾔ' => 'ᾔ', + 'ᾕ' => 'ᾕ', + 'ᾖ' => 'ᾖ', + 'ᾗ' => 'ᾗ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾠ', + 'ᾡ' => 'ᾡ', + 'ᾢ' => 'ᾢ', + 'ᾣ' => 'ᾣ', + 'ᾤ' => 'ᾤ', + 'ᾥ' => 'ᾥ', + 'ᾦ' => 'ᾦ', + 'ᾧ' => 'ᾧ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾰ' => 'ᾰ', + 'ᾱ' => 'ᾱ', + 'ᾲ' => 'ᾲ', + 'ᾳ' => 'ᾳ', + 'ᾴ' => 'ᾴ', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾷ', + 'Ᾰ' => 'Ᾰ', + 'Ᾱ' => 'Ᾱ', + 'Ὰ' => 'Ὰ', + 'Ά' => 'Ά', + 'ᾼ' => 'ᾼ', + 'ι' => 'ι', + '῁' => '῁', + 'ῂ' => 'ῂ', + 'ῃ' => 'ῃ', + 'ῄ' => 'ῄ', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῇ', + 'Ὲ' => 'Ὲ', + 'Έ' => 'Έ', + 'Ὴ' => 'Ὴ', + 'Ή' => 'Ή', + 'ῌ' => 'ῌ', + '῍' => '῍', + '῎' => '῎', + '῏' => '῏', + 'ῐ' => 'ῐ', + 'ῑ' => 'ῑ', + 'ῒ' => 'ῒ', + 'ΐ' => 'ΐ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'Ῐ' => 'Ῐ', + 'Ῑ' => 'Ῑ', + 'Ὶ' => 'Ὶ', + 'Ί' => 'Ί', + '῝' => '῝', + '῞' => '῞', + '῟' => '῟', + 'ῠ' => 'ῠ', + 'ῡ' => 'ῡ', + 'ῢ' => 'ῢ', + 'ΰ' => 'ΰ', + 'ῤ' => 'ῤ', + 'ῥ' => 'ῥ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'Ῠ' => 'Ῠ', + 'Ῡ' => 'Ῡ', + 'Ὺ' => 'Ὺ', + 'Ύ' => 'Ύ', + 'Ῥ' => 'Ῥ', + '῭' => '῭', + '΅' => '΅', + '`' => '`', + 'ῲ' => 'ῲ', + 'ῳ' => 'ῳ', + 'ῴ' => 'ῴ', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῷ', + 'Ὸ' => 'Ὸ', + 'Ό' => 'Ό', + 'Ὼ' => 'Ὼ', + 'Ώ' => 'Ώ', + 'ῼ' => 'ῼ', + '´' => '´', + ' ' => ' ', + ' ' => ' ', + 'Ω' => 'Ω', + 'K' => 'K', + 'Å' => 'Å', + '↚' => '↚', + '↛' => '↛', + '↮' => '↮', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '∄' => '∄', + '∉' => '∉', + '∌' => '∌', + '∤' => '∤', + '∦' => '∦', + '≁' => '≁', + '≄' => '≄', + '≇' => '≇', + '≉' => '≉', + '≠' => '≠', + '≢' => '≢', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≴' => '≴', + '≵' => '≵', + '≸' => '≸', + '≹' => '≹', + '⊀' => '⊀', + '⊁' => '⊁', + '⊄' => '⊄', + '⊅' => '⊅', + '⊈' => '⊈', + '⊉' => '⊉', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + '〈' => '〈', + '〉' => '〉', + '⫝̸' => '⫝̸', + 'が' => 'が', + 'ぎ' => 'ぎ', + 'ぐ' => 'ぐ', + 'げ' => 'げ', + 'ご' => 'ご', + 'ざ' => 'ざ', + 'じ' => 'じ', + 'ず' => 'ず', + 'ぜ' => 'ぜ', + 'ぞ' => 'ぞ', + 'だ' => 'だ', + 'ぢ' => 'ぢ', + 'づ' => 'づ', + 'で' => 'で', + 'ど' => 'ど', + 'ば' => 'ば', + 'ぱ' => 'ぱ', + 'び' => 'び', + 'ぴ' => 'ぴ', + 'ぶ' => 'ぶ', + 'ぷ' => 'ぷ', + 'べ' => 'べ', + 'ぺ' => 'ぺ', + 'ぼ' => 'ぼ', + 'ぽ' => 'ぽ', + 'ゔ' => 'ゔ', + 'ゞ' => 'ゞ', + 'ガ' => 'ガ', + 'ギ' => 'ギ', + 'グ' => 'グ', + 'ゲ' => 'ゲ', + 'ゴ' => 'ゴ', + 'ザ' => 'ザ', + 'ジ' => 'ジ', + 'ズ' => 'ズ', + 'ゼ' => 'ゼ', + 'ゾ' => 'ゾ', + 'ダ' => 'ダ', + 'ヂ' => 'ヂ', + 'ヅ' => 'ヅ', + 'デ' => 'デ', + 'ド' => 'ド', + 'バ' => 'バ', + 'パ' => 'パ', + 'ビ' => 'ビ', + 'ピ' => 'ピ', + 'ブ' => 'ブ', + 'プ' => 'プ', + 'ベ' => 'ベ', + 'ペ' => 'ペ', + 'ボ' => 'ボ', + 'ポ' => 'ポ', + 'ヴ' => 'ヴ', + 'ヷ' => 'ヷ', + 'ヸ' => 'ヸ', + 'ヹ' => 'ヹ', + 'ヺ' => 'ヺ', + 'ヾ' => 'ヾ', + '豈' => '豈', + '更' => '更', + '車' => '車', + '賈' => '賈', + '滑' => '滑', + '串' => '串', + '句' => '句', + '龜' => '龜', + '龜' => '龜', + '契' => '契', + '金' => '金', + '喇' => '喇', + '奈' => '奈', + '懶' => '懶', + '癩' => '癩', + '羅' => '羅', + '蘿' => '蘿', + '螺' => '螺', + '裸' => '裸', + '邏' => '邏', + '樂' => '樂', + '洛' => '洛', + '烙' => '烙', + '珞' => '珞', + '落' => '落', + '酪' => '酪', + '駱' => '駱', + '亂' => '亂', + '卵' => '卵', + '欄' => '欄', + '爛' => '爛', + '蘭' => '蘭', + '鸞' => '鸞', + '嵐' => '嵐', + '濫' => '濫', + '藍' => '藍', + '襤' => '襤', + '拉' => '拉', + '臘' => '臘', + '蠟' => '蠟', + '廊' => '廊', + '朗' => '朗', + '浪' => '浪', + '狼' => '狼', + '郎' => '郎', + '來' => '來', + '冷' => '冷', + '勞' => '勞', + '擄' => '擄', + '櫓' => '櫓', + '爐' => '爐', + '盧' => '盧', + '老' => '老', + '蘆' => '蘆', + '虜' => '虜', + '路' => '路', + '露' => '露', + '魯' => '魯', + '鷺' => '鷺', + '碌' => '碌', + '祿' => '祿', + '綠' => '綠', + '菉' => '菉', + '錄' => '錄', + '鹿' => '鹿', + '論' => '論', + '壟' => '壟', + '弄' => '弄', + '籠' => '籠', + '聾' => '聾', + '牢' => '牢', + '磊' => '磊', + '賂' => '賂', + '雷' => '雷', + '壘' => '壘', + '屢' => '屢', + '樓' => '樓', + '淚' => '淚', + '漏' => '漏', + '累' => '累', + '縷' => '縷', + '陋' => '陋', + '勒' => '勒', + '肋' => '肋', + '凜' => '凜', + '凌' => '凌', + '稜' => '稜', + '綾' => '綾', + '菱' => '菱', + '陵' => '陵', + '讀' => '讀', + '拏' => '拏', + '樂' => '樂', + '諾' => '諾', + '丹' => '丹', + '寧' => '寧', + '怒' => '怒', + '率' => '率', + '異' => '異', + '北' => '北', + '磻' => '磻', + '便' => '便', + '復' => '復', + '不' => '不', + '泌' => '泌', + '數' => '數', + '索' => '索', + '參' => '參', + '塞' => '塞', + '省' => '省', + '葉' => '葉', + '說' => '說', + '殺' => '殺', + '辰' => '辰', + '沈' => '沈', + '拾' => '拾', + '若' => '若', + '掠' => '掠', + '略' => '略', + '亮' => '亮', + '兩' => '兩', + '凉' => '凉', + '梁' => '梁', + '糧' => '糧', + '良' => '良', + '諒' => '諒', + '量' => '量', + '勵' => '勵', + '呂' => '呂', + '女' => '女', + '廬' => '廬', + '旅' => '旅', + '濾' => '濾', + '礪' => '礪', + '閭' => '閭', + '驪' => '驪', + '麗' => '麗', + '黎' => '黎', + '力' => '力', + '曆' => '曆', + '歷' => '歷', + '轢' => '轢', + '年' => '年', + '憐' => '憐', + '戀' => '戀', + '撚' => '撚', + '漣' => '漣', + '煉' => '煉', + '璉' => '璉', + '秊' => '秊', + '練' => '練', + '聯' => '聯', + '輦' => '輦', + '蓮' => '蓮', + '連' => '連', + '鍊' => '鍊', + '列' => '列', + '劣' => '劣', + '咽' => '咽', + '烈' => '烈', + '裂' => '裂', + '說' => '說', + '廉' => '廉', + '念' => '念', + '捻' => '捻', + '殮' => '殮', + '簾' => '簾', + '獵' => '獵', + '令' => '令', + '囹' => '囹', + '寧' => '寧', + '嶺' => '嶺', + '怜' => '怜', + '玲' => '玲', + '瑩' => '瑩', + '羚' => '羚', + '聆' => '聆', + '鈴' => '鈴', + '零' => '零', + '靈' => '靈', + '領' => '領', + '例' => '例', + '禮' => '禮', + '醴' => '醴', + '隸' => '隸', + '惡' => '惡', + '了' => '了', + '僚' => '僚', + '寮' => '寮', + '尿' => '尿', + '料' => '料', + '樂' => '樂', + '燎' => '燎', + '療' => '療', + '蓼' => '蓼', + '遼' => '遼', + '龍' => '龍', + '暈' => '暈', + '阮' => '阮', + '劉' => '劉', + '杻' => '杻', + '柳' => '柳', + '流' => '流', + '溜' => '溜', + '琉' => '琉', + '留' => '留', + '硫' => '硫', + '紐' => '紐', + '類' => '類', + '六' => '六', + '戮' => '戮', + '陸' => '陸', + '倫' => '倫', + '崙' => '崙', + '淪' => '淪', + '輪' => '輪', + '律' => '律', + '慄' => '慄', + '栗' => '栗', + '率' => '率', + '隆' => '隆', + '利' => '利', + '吏' => '吏', + '履' => '履', + '易' => '易', + '李' => '李', + '梨' => '梨', + '泥' => '泥', + '理' => '理', + '痢' => '痢', + '罹' => '罹', + '裏' => '裏', + '裡' => '裡', + '里' => '里', + '離' => '離', + '匿' => '匿', + '溺' => '溺', + '吝' => '吝', + '燐' => '燐', + '璘' => '璘', + '藺' => '藺', + '隣' => '隣', + '鱗' => '鱗', + '麟' => '麟', + '林' => '林', + '淋' => '淋', + '臨' => '臨', + '立' => '立', + '笠' => '笠', + '粒' => '粒', + '狀' => '狀', + '炙' => '炙', + '識' => '識', + '什' => '什', + '茶' => '茶', + '刺' => '刺', + '切' => '切', + '度' => '度', + '拓' => '拓', + '糖' => '糖', + '宅' => '宅', + '洞' => '洞', + '暴' => '暴', + '輻' => '輻', + '行' => '行', + '降' => '降', + '見' => '見', + '廓' => '廓', + '兀' => '兀', + '嗀' => '嗀', + '塚' => '塚', + '晴' => '晴', + '凞' => '凞', + '猪' => '猪', + '益' => '益', + '礼' => '礼', + '神' => '神', + '祥' => '祥', + '福' => '福', + '靖' => '靖', + '精' => '精', + '羽' => '羽', + '蘒' => '蘒', + '諸' => '諸', + '逸' => '逸', + '都' => '都', + '飯' => '飯', + '飼' => '飼', + '館' => '館', + '鶴' => '鶴', + '郞' => '郞', + '隷' => '隷', + '侮' => '侮', + '僧' => '僧', + '免' => '免', + '勉' => '勉', + '勤' => '勤', + '卑' => '卑', + '喝' => '喝', + '嘆' => '嘆', + '器' => '器', + '塀' => '塀', + '墨' => '墨', + '層' => '層', + '屮' => '屮', + '悔' => '悔', + '慨' => '慨', + '憎' => '憎', + '懲' => '懲', + '敏' => '敏', + '既' => '既', + '暑' => '暑', + '梅' => '梅', + '海' => '海', + '渚' => '渚', + '漢' => '漢', + '煮' => '煮', + '爫' => '爫', + '琢' => '琢', + '碑' => '碑', + '社' => '社', + '祉' => '祉', + '祈' => '祈', + '祐' => '祐', + '祖' => '祖', + '祝' => '祝', + '禍' => '禍', + '禎' => '禎', + '穀' => '穀', + '突' => '突', + '節' => '節', + '練' => '練', + '縉' => '縉', + '繁' => '繁', + '署' => '署', + '者' => '者', + '臭' => '臭', + '艹' => '艹', + '艹' => '艹', + '著' => '著', + '褐' => '褐', + '視' => '視', + '謁' => '謁', + '謹' => '謹', + '賓' => '賓', + '贈' => '贈', + '辶' => '辶', + '逸' => '逸', + '難' => '難', + '響' => '響', + '頻' => '頻', + '恵' => '恵', + '𤋮' => '𤋮', + '舘' => '舘', + '並' => '並', + '况' => '况', + '全' => '全', + '侀' => '侀', + '充' => '充', + '冀' => '冀', + '勇' => '勇', + '勺' => '勺', + '喝' => '喝', + '啕' => '啕', + '喙' => '喙', + '嗢' => '嗢', + '塚' => '塚', + '墳' => '墳', + '奄' => '奄', + '奔' => '奔', + '婢' => '婢', + '嬨' => '嬨', + '廒' => '廒', + '廙' => '廙', + '彩' => '彩', + '徭' => '徭', + '惘' => '惘', + '慎' => '慎', + '愈' => '愈', + '憎' => '憎', + '慠' => '慠', + '懲' => '懲', + '戴' => '戴', + '揄' => '揄', + '搜' => '搜', + '摒' => '摒', + '敖' => '敖', + '晴' => '晴', + '朗' => '朗', + '望' => '望', + '杖' => '杖', + '歹' => '歹', + '殺' => '殺', + '流' => '流', + '滛' => '滛', + '滋' => '滋', + '漢' => '漢', + '瀞' => '瀞', + '煮' => '煮', + '瞧' => '瞧', + '爵' => '爵', + '犯' => '犯', + '猪' => '猪', + '瑱' => '瑱', + '甆' => '甆', + '画' => '画', + '瘝' => '瘝', + '瘟' => '瘟', + '益' => '益', + '盛' => '盛', + '直' => '直', + '睊' => '睊', + '着' => '着', + '磌' => '磌', + '窱' => '窱', + '節' => '節', + '类' => '类', + '絛' => '絛', + '練' => '練', + '缾' => '缾', + '者' => '者', + '荒' => '荒', + '華' => '華', + '蝹' => '蝹', + '襁' => '襁', + '覆' => '覆', + '視' => '視', + '調' => '調', + '諸' => '諸', + '請' => '請', + '謁' => '謁', + '諾' => '諾', + '諭' => '諭', + '謹' => '謹', + '變' => '變', + '贈' => '贈', + '輸' => '輸', + '遲' => '遲', + '醙' => '醙', + '鉶' => '鉶', + '陼' => '陼', + '難' => '難', + '靖' => '靖', + '韛' => '韛', + '響' => '響', + '頋' => '頋', + '頻' => '頻', + '鬒' => '鬒', + '龜' => '龜', + '𢡊' => '𢡊', + '𢡄' => '𢡄', + '𣏕' => '𣏕', + '㮝' => '㮝', + '䀘' => '䀘', + '䀹' => '䀹', + '𥉉' => '𥉉', + '𥳐' => '𥳐', + '𧻓' => '𧻓', + '齃' => '齃', + '龎' => '龎', + 'יִ' => 'יִ', + 'ײַ' => 'ײַ', + 'שׁ' => 'שׁ', + 'שׂ' => 'שׂ', + 'שּׁ' => 'שּׁ', + 'שּׂ' => 'שּׂ', + 'אַ' => 'אַ', + 'אָ' => 'אָ', + 'אּ' => 'אּ', + 'בּ' => 'בּ', + 'גּ' => 'גּ', + 'דּ' => 'דּ', + 'הּ' => 'הּ', + 'וּ' => 'וּ', + 'זּ' => 'זּ', + 'טּ' => 'טּ', + 'יּ' => 'יּ', + 'ךּ' => 'ךּ', + 'כּ' => 'כּ', + 'לּ' => 'לּ', + 'מּ' => 'מּ', + 'נּ' => 'נּ', + 'סּ' => 'סּ', + 'ףּ' => 'ףּ', + 'פּ' => 'פּ', + 'צּ' => 'צּ', + 'קּ' => 'קּ', + 'רּ' => 'רּ', + 'שּ' => 'שּ', + 'תּ' => 'תּ', + 'וֹ' => 'וֹ', + 'בֿ' => 'בֿ', + 'כֿ' => 'כֿ', + 'פֿ' => 'פֿ', + '𑂚' => '𑂚', + '𑂜' => '𑂜', + '𑂫' => '𑂫', + '𑄮' => '𑄮', + '𑄯' => '𑄯', + '𑍋' => '𑍋', + '𑍌' => '𑍌', + '𑒻' => '𑒻', + '𑒼' => '𑒼', + '𑒾' => '𑒾', + '𑖺' => '𑖺', + '𑖻' => '𑖻', + '𑤸' => '𑤸', + '𝅗𝅥' => '𝅗𝅥', + '𝅘𝅥' => '𝅘𝅥', + '𝅘𝅥𝅮' => '𝅘𝅥𝅮', + '𝅘𝅥𝅯' => '𝅘𝅥𝅯', + '𝅘𝅥𝅰' => '𝅘𝅥𝅰', + '𝅘𝅥𝅱' => '𝅘𝅥𝅱', + '𝅘𝅥𝅲' => '𝅘𝅥𝅲', + '𝆹𝅥' => '𝆹𝅥', + '𝆺𝅥' => '𝆺𝅥', + '𝆹𝅥𝅮' => '𝆹𝅥𝅮', + '𝆺𝅥𝅮' => '𝆺𝅥𝅮', + '𝆹𝅥𝅯' => '𝆹𝅥𝅯', + '𝆺𝅥𝅯' => '𝆺𝅥𝅯', + '丽' => '丽', + '丸' => '丸', + '乁' => '乁', + '𠄢' => '𠄢', + '你' => '你', + '侮' => '侮', + '侻' => '侻', + '倂' => '倂', + '偺' => '偺', + '備' => '備', + '僧' => '僧', + '像' => '像', + '㒞' => '㒞', + '𠘺' => '𠘺', + '免' => '免', + '兔' => '兔', + '兤' => '兤', + '具' => '具', + '𠔜' => '𠔜', + '㒹' => '㒹', + '內' => '內', + '再' => '再', + '𠕋' => '𠕋', + '冗' => '冗', + '冤' => '冤', + '仌' => '仌', + '冬' => '冬', + '况' => '况', + '𩇟' => '𩇟', + '凵' => '凵', + '刃' => '刃', + '㓟' => '㓟', + '刻' => '刻', + '剆' => '剆', + '割' => '割', + '剷' => '剷', + '㔕' => '㔕', + '勇' => '勇', + '勉' => '勉', + '勤' => '勤', + '勺' => '勺', + '包' => '包', + '匆' => '匆', + '北' => '北', + '卉' => '卉', + '卑' => '卑', + '博' => '博', + '即' => '即', + '卽' => '卽', + '卿' => '卿', + '卿' => '卿', + '卿' => '卿', + '𠨬' => '𠨬', + '灰' => '灰', + '及' => '及', + '叟' => '叟', + '𠭣' => '𠭣', + '叫' => '叫', + '叱' => '叱', + '吆' => '吆', + '咞' => '咞', + '吸' => '吸', + '呈' => '呈', + '周' => '周', + '咢' => '咢', + '哶' => '哶', + '唐' => '唐', + '啓' => '啓', + '啣' => '啣', + '善' => '善', + '善' => '善', + '喙' => '喙', + '喫' => '喫', + '喳' => '喳', + '嗂' => '嗂', + '圖' => '圖', + '嘆' => '嘆', + '圗' => '圗', + '噑' => '噑', + '噴' => '噴', + '切' => '切', + '壮' => '壮', + '城' => '城', + '埴' => '埴', + '堍' => '堍', + '型' => '型', + '堲' => '堲', + '報' => '報', + '墬' => '墬', + '𡓤' => '𡓤', + '売' => '売', + '壷' => '壷', + '夆' => '夆', + '多' => '多', + '夢' => '夢', + '奢' => '奢', + '𡚨' => '𡚨', + '𡛪' => '𡛪', + '姬' => '姬', + '娛' => '娛', + '娧' => '娧', + '姘' => '姘', + '婦' => '婦', + '㛮' => '㛮', + '㛼' => '㛼', + '嬈' => '嬈', + '嬾' => '嬾', + '嬾' => '嬾', + '𡧈' => '𡧈', + '寃' => '寃', + '寘' => '寘', + '寧' => '寧', + '寳' => '寳', + '𡬘' => '𡬘', + '寿' => '寿', + '将' => '将', + '当' => '当', + '尢' => '尢', + '㞁' => '㞁', + '屠' => '屠', + '屮' => '屮', + '峀' => '峀', + '岍' => '岍', + '𡷤' => '𡷤', + '嵃' => '嵃', + '𡷦' => '𡷦', + '嵮' => '嵮', + '嵫' => '嵫', + '嵼' => '嵼', + '巡' => '巡', + '巢' => '巢', + '㠯' => '㠯', + '巽' => '巽', + '帨' => '帨', + '帽' => '帽', + '幩' => '幩', + '㡢' => '㡢', + '𢆃' => '𢆃', + '㡼' => '㡼', + '庰' => '庰', + '庳' => '庳', + '庶' => '庶', + '廊' => '廊', + '𪎒' => '𪎒', + '廾' => '廾', + '𢌱' => '𢌱', + '𢌱' => '𢌱', + '舁' => '舁', + '弢' => '弢', + '弢' => '弢', + '㣇' => '㣇', + '𣊸' => '𣊸', + '𦇚' => '𦇚', + '形' => '形', + '彫' => '彫', + '㣣' => '㣣', + '徚' => '徚', + '忍' => '忍', + '志' => '志', + '忹' => '忹', + '悁' => '悁', + '㤺' => '㤺', + '㤜' => '㤜', + '悔' => '悔', + '𢛔' => '𢛔', + '惇' => '惇', + '慈' => '慈', + '慌' => '慌', + '慎' => '慎', + '慌' => '慌', + '慺' => '慺', + '憎' => '憎', + '憲' => '憲', + '憤' => '憤', + '憯' => '憯', + '懞' => '懞', + '懲' => '懲', + '懶' => '懶', + '成' => '成', + '戛' => '戛', + '扝' => '扝', + '抱' => '抱', + '拔' => '拔', + '捐' => '捐', + '𢬌' => '𢬌', + '挽' => '挽', + '拼' => '拼', + '捨' => '捨', + '掃' => '掃', + '揤' => '揤', + '𢯱' => '𢯱', + '搢' => '搢', + '揅' => '揅', + '掩' => '掩', + '㨮' => '㨮', + '摩' => '摩', + '摾' => '摾', + '撝' => '撝', + '摷' => '摷', + '㩬' => '㩬', + '敏' => '敏', + '敬' => '敬', + '𣀊' => '𣀊', + '旣' => '旣', + '書' => '書', + '晉' => '晉', + '㬙' => '㬙', + '暑' => '暑', + '㬈' => '㬈', + '㫤' => '㫤', + '冒' => '冒', + '冕' => '冕', + '最' => '最', + '暜' => '暜', + '肭' => '肭', + '䏙' => '䏙', + '朗' => '朗', + '望' => '望', + '朡' => '朡', + '杞' => '杞', + '杓' => '杓', + '𣏃' => '𣏃', + '㭉' => '㭉', + '柺' => '柺', + '枅' => '枅', + '桒' => '桒', + '梅' => '梅', + '𣑭' => '𣑭', + '梎' => '梎', + '栟' => '栟', + '椔' => '椔', + '㮝' => '㮝', + '楂' => '楂', + '榣' => '榣', + '槪' => '槪', + '檨' => '檨', + '𣚣' => '𣚣', + '櫛' => '櫛', + '㰘' => '㰘', + '次' => '次', + '𣢧' => '𣢧', + '歔' => '歔', + '㱎' => '㱎', + '歲' => '歲', + '殟' => '殟', + '殺' => '殺', + '殻' => '殻', + '𣪍' => '𣪍', + '𡴋' => '𡴋', + '𣫺' => '𣫺', + '汎' => '汎', + '𣲼' => '𣲼', + '沿' => '沿', + '泍' => '泍', + '汧' => '汧', + '洖' => '洖', + '派' => '派', + '海' => '海', + '流' => '流', + '浩' => '浩', + '浸' => '浸', + '涅' => '涅', + '𣴞' => '𣴞', + '洴' => '洴', + '港' => '港', + '湮' => '湮', + '㴳' => '㴳', + '滋' => '滋', + '滇' => '滇', + '𣻑' => '𣻑', + '淹' => '淹', + '潮' => '潮', + '𣽞' => '𣽞', + '𣾎' => '𣾎', + '濆' => '濆', + '瀹' => '瀹', + '瀞' => '瀞', + '瀛' => '瀛', + '㶖' => '㶖', + '灊' => '灊', + '災' => '災', + '灷' => '灷', + '炭' => '炭', + '𠔥' => '𠔥', + '煅' => '煅', + '𤉣' => '𤉣', + '熜' => '熜', + '𤎫' => '𤎫', + '爨' => '爨', + '爵' => '爵', + '牐' => '牐', + '𤘈' => '𤘈', + '犀' => '犀', + '犕' => '犕', + '𤜵' => '𤜵', + '𤠔' => '𤠔', + '獺' => '獺', + '王' => '王', + '㺬' => '㺬', + '玥' => '玥', + '㺸' => '㺸', + '㺸' => '㺸', + '瑇' => '瑇', + '瑜' => '瑜', + '瑱' => '瑱', + '璅' => '璅', + '瓊' => '瓊', + '㼛' => '㼛', + '甤' => '甤', + '𤰶' => '𤰶', + '甾' => '甾', + '𤲒' => '𤲒', + '異' => '異', + '𢆟' => '𢆟', + '瘐' => '瘐', + '𤾡' => '𤾡', + '𤾸' => '𤾸', + '𥁄' => '𥁄', + '㿼' => '㿼', + '䀈' => '䀈', + '直' => '直', + '𥃳' => '𥃳', + '𥃲' => '𥃲', + '𥄙' => '𥄙', + '𥄳' => '𥄳', + '眞' => '眞', + '真' => '真', + '真' => '真', + '睊' => '睊', + '䀹' => '䀹', + '瞋' => '瞋', + '䁆' => '䁆', + '䂖' => '䂖', + '𥐝' => '𥐝', + '硎' => '硎', + '碌' => '碌', + '磌' => '磌', + '䃣' => '䃣', + '𥘦' => '𥘦', + '祖' => '祖', + '𥚚' => '𥚚', + '𥛅' => '𥛅', + '福' => '福', + '秫' => '秫', + '䄯' => '䄯', + '穀' => '穀', + '穊' => '穊', + '穏' => '穏', + '𥥼' => '𥥼', + '𥪧' => '𥪧', + '𥪧' => '𥪧', + '竮' => '竮', + '䈂' => '䈂', + '𥮫' => '𥮫', + '篆' => '篆', + '築' => '築', + '䈧' => '䈧', + '𥲀' => '𥲀', + '糒' => '糒', + '䊠' => '䊠', + '糨' => '糨', + '糣' => '糣', + '紀' => '紀', + '𥾆' => '𥾆', + '絣' => '絣', + '䌁' => '䌁', + '緇' => '緇', + '縂' => '縂', + '繅' => '繅', + '䌴' => '䌴', + '𦈨' => '𦈨', + '𦉇' => '𦉇', + '䍙' => '䍙', + '𦋙' => '𦋙', + '罺' => '罺', + '𦌾' => '𦌾', + '羕' => '羕', + '翺' => '翺', + '者' => '者', + '𦓚' => '𦓚', + '𦔣' => '𦔣', + '聠' => '聠', + '𦖨' => '𦖨', + '聰' => '聰', + '𣍟' => '𣍟', + '䏕' => '䏕', + '育' => '育', + '脃' => '脃', + '䐋' => '䐋', + '脾' => '脾', + '媵' => '媵', + '𦞧' => '𦞧', + '𦞵' => '𦞵', + '𣎓' => '𣎓', + '𣎜' => '𣎜', + '舁' => '舁', + '舄' => '舄', + '辞' => '辞', + '䑫' => '䑫', + '芑' => '芑', + '芋' => '芋', + '芝' => '芝', + '劳' => '劳', + '花' => '花', + '芳' => '芳', + '芽' => '芽', + '苦' => '苦', + '𦬼' => '𦬼', + '若' => '若', + '茝' => '茝', + '荣' => '荣', + '莭' => '莭', + '茣' => '茣', + '莽' => '莽', + '菧' => '菧', + '著' => '著', + '荓' => '荓', + '菊' => '菊', + '菌' => '菌', + '菜' => '菜', + '𦰶' => '𦰶', + '𦵫' => '𦵫', + '𦳕' => '𦳕', + '䔫' => '䔫', + '蓱' => '蓱', + '蓳' => '蓳', + '蔖' => '蔖', + '𧏊' => '𧏊', + '蕤' => '蕤', + '𦼬' => '𦼬', + '䕝' => '䕝', + '䕡' => '䕡', + '𦾱' => '𦾱', + '𧃒' => '𧃒', + '䕫' => '䕫', + '虐' => '虐', + '虜' => '虜', + '虧' => '虧', + '虩' => '虩', + '蚩' => '蚩', + '蚈' => '蚈', + '蜎' => '蜎', + '蛢' => '蛢', + '蝹' => '蝹', + '蜨' => '蜨', + '蝫' => '蝫', + '螆' => '螆', + '䗗' => '䗗', + '蟡' => '蟡', + '蠁' => '蠁', + '䗹' => '䗹', + '衠' => '衠', + '衣' => '衣', + '𧙧' => '𧙧', + '裗' => '裗', + '裞' => '裞', + '䘵' => '䘵', + '裺' => '裺', + '㒻' => '㒻', + '𧢮' => '𧢮', + '𧥦' => '𧥦', + '䚾' => '䚾', + '䛇' => '䛇', + '誠' => '誠', + '諭' => '諭', + '變' => '變', + '豕' => '豕', + '𧲨' => '𧲨', + '貫' => '貫', + '賁' => '賁', + '贛' => '贛', + '起' => '起', + '𧼯' => '𧼯', + '𠠄' => '𠠄', + '跋' => '跋', + '趼' => '趼', + '跰' => '跰', + '𠣞' => '𠣞', + '軔' => '軔', + '輸' => '輸', + '𨗒' => '𨗒', + '𨗭' => '𨗭', + '邔' => '邔', + '郱' => '郱', + '鄑' => '鄑', + '𨜮' => '𨜮', + '鄛' => '鄛', + '鈸' => '鈸', + '鋗' => '鋗', + '鋘' => '鋘', + '鉼' => '鉼', + '鏹' => '鏹', + '鐕' => '鐕', + '𨯺' => '𨯺', + '開' => '開', + '䦕' => '䦕', + '閷' => '閷', + '𨵷' => '𨵷', + '䧦' => '䧦', + '雃' => '雃', + '嶲' => '嶲', + '霣' => '霣', + '𩅅' => '𩅅', + '𩈚' => '𩈚', + '䩮' => '䩮', + '䩶' => '䩶', + '韠' => '韠', + '𩐊' => '𩐊', + '䪲' => '䪲', + '𩒖' => '𩒖', + '頋' => '頋', + '頋' => '頋', + '頩' => '頩', + '𩖶' => '𩖶', + '飢' => '飢', + '䬳' => '䬳', + '餩' => '餩', + '馧' => '馧', + '駂' => '駂', + '駾' => '駾', + '䯎' => '䯎', + '𩬰' => '𩬰', + '鬒' => '鬒', + '鱀' => '鱀', + '鳽' => '鳽', + '䳎' => '䳎', + '䳭' => '䳭', + '鵧' => '鵧', + '𪃎' => '𪃎', + '䳸' => '䳸', + '𪄅' => '𪄅', + '𪈎' => '𪈎', + '𪊑' => '𪊑', + '麻' => '麻', + '䵖' => '䵖', + '黹' => '黹', + '黾' => '黾', + '鼅' => '鼅', + '鼏' => '鼏', + '鼖' => '鼖', + '鼻' => '鼻', + '𪘀' => '𪘀', +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php new file mode 100644 index 0000000..ec90f36 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php @@ -0,0 +1,876 @@ + 230, + '́' => 230, + '̂' => 230, + '̃' => 230, + '̄' => 230, + '̅' => 230, + '̆' => 230, + '̇' => 230, + '̈' => 230, + '̉' => 230, + '̊' => 230, + '̋' => 230, + '̌' => 230, + '̍' => 230, + '̎' => 230, + '̏' => 230, + '̐' => 230, + '̑' => 230, + '̒' => 230, + '̓' => 230, + '̔' => 230, + '̕' => 232, + '̖' => 220, + '̗' => 220, + '̘' => 220, + '̙' => 220, + '̚' => 232, + '̛' => 216, + '̜' => 220, + '̝' => 220, + '̞' => 220, + '̟' => 220, + '̠' => 220, + '̡' => 202, + '̢' => 202, + '̣' => 220, + '̤' => 220, + '̥' => 220, + '̦' => 220, + '̧' => 202, + '̨' => 202, + '̩' => 220, + '̪' => 220, + '̫' => 220, + '̬' => 220, + '̭' => 220, + '̮' => 220, + '̯' => 220, + '̰' => 220, + '̱' => 220, + '̲' => 220, + '̳' => 220, + '̴' => 1, + '̵' => 1, + '̶' => 1, + '̷' => 1, + '̸' => 1, + '̹' => 220, + '̺' => 220, + '̻' => 220, + '̼' => 220, + '̽' => 230, + '̾' => 230, + '̿' => 230, + '̀' => 230, + '́' => 230, + '͂' => 230, + '̓' => 230, + '̈́' => 230, + 'ͅ' => 240, + '͆' => 230, + '͇' => 220, + '͈' => 220, + '͉' => 220, + '͊' => 230, + '͋' => 230, + '͌' => 230, + '͍' => 220, + '͎' => 220, + '͐' => 230, + '͑' => 230, + '͒' => 230, + '͓' => 220, + '͔' => 220, + '͕' => 220, + '͖' => 220, + '͗' => 230, + '͘' => 232, + '͙' => 220, + '͚' => 220, + '͛' => 230, + '͜' => 233, + '͝' => 234, + '͞' => 234, + '͟' => 233, + '͠' => 234, + '͡' => 234, + '͢' => 233, + 'ͣ' => 230, + 'ͤ' => 230, + 'ͥ' => 230, + 'ͦ' => 230, + 'ͧ' => 230, + 'ͨ' => 230, + 'ͩ' => 230, + 'ͪ' => 230, + 'ͫ' => 230, + 'ͬ' => 230, + 'ͭ' => 230, + 'ͮ' => 230, + 'ͯ' => 230, + '҃' => 230, + '҄' => 230, + '҅' => 230, + '҆' => 230, + '҇' => 230, + '֑' => 220, + '֒' => 230, + '֓' => 230, + '֔' => 230, + '֕' => 230, + '֖' => 220, + '֗' => 230, + '֘' => 230, + '֙' => 230, + '֚' => 222, + '֛' => 220, + '֜' => 230, + '֝' => 230, + '֞' => 230, + '֟' => 230, + '֠' => 230, + '֡' => 230, + '֢' => 220, + '֣' => 220, + '֤' => 220, + '֥' => 220, + '֦' => 220, + '֧' => 220, + '֨' => 230, + '֩' => 230, + '֪' => 220, + '֫' => 230, + '֬' => 230, + '֭' => 222, + '֮' => 228, + '֯' => 230, + 'ְ' => 10, + 'ֱ' => 11, + 'ֲ' => 12, + 'ֳ' => 13, + 'ִ' => 14, + 'ֵ' => 15, + 'ֶ' => 16, + 'ַ' => 17, + 'ָ' => 18, + 'ֹ' => 19, + 'ֺ' => 19, + 'ֻ' => 20, + 'ּ' => 21, + 'ֽ' => 22, + 'ֿ' => 23, + 'ׁ' => 24, + 'ׂ' => 25, + 'ׄ' => 230, + 'ׅ' => 220, + 'ׇ' => 18, + 'ؐ' => 230, + 'ؑ' => 230, + 'ؒ' => 230, + 'ؓ' => 230, + 'ؔ' => 230, + 'ؕ' => 230, + 'ؖ' => 230, + 'ؗ' => 230, + 'ؘ' => 30, + 'ؙ' => 31, + 'ؚ' => 32, + 'ً' => 27, + 'ٌ' => 28, + 'ٍ' => 29, + 'َ' => 30, + 'ُ' => 31, + 'ِ' => 32, + 'ّ' => 33, + 'ْ' => 34, + 'ٓ' => 230, + 'ٔ' => 230, + 'ٕ' => 220, + 'ٖ' => 220, + 'ٗ' => 230, + '٘' => 230, + 'ٙ' => 230, + 'ٚ' => 230, + 'ٛ' => 230, + 'ٜ' => 220, + 'ٝ' => 230, + 'ٞ' => 230, + 'ٟ' => 220, + 'ٰ' => 35, + 'ۖ' => 230, + 'ۗ' => 230, + 'ۘ' => 230, + 'ۙ' => 230, + 'ۚ' => 230, + 'ۛ' => 230, + 'ۜ' => 230, + '۟' => 230, + '۠' => 230, + 'ۡ' => 230, + 'ۢ' => 230, + 'ۣ' => 220, + 'ۤ' => 230, + 'ۧ' => 230, + 'ۨ' => 230, + '۪' => 220, + '۫' => 230, + '۬' => 230, + 'ۭ' => 220, + 'ܑ' => 36, + 'ܰ' => 230, + 'ܱ' => 220, + 'ܲ' => 230, + 'ܳ' => 230, + 'ܴ' => 220, + 'ܵ' => 230, + 'ܶ' => 230, + 'ܷ' => 220, + 'ܸ' => 220, + 'ܹ' => 220, + 'ܺ' => 230, + 'ܻ' => 220, + 'ܼ' => 220, + 'ܽ' => 230, + 'ܾ' => 220, + 'ܿ' => 230, + '݀' => 230, + '݁' => 230, + '݂' => 220, + '݃' => 230, + '݄' => 220, + '݅' => 230, + '݆' => 220, + '݇' => 230, + '݈' => 220, + '݉' => 230, + '݊' => 230, + '߫' => 230, + '߬' => 230, + '߭' => 230, + '߮' => 230, + '߯' => 230, + '߰' => 230, + '߱' => 230, + '߲' => 220, + '߳' => 230, + '߽' => 220, + 'ࠖ' => 230, + 'ࠗ' => 230, + '࠘' => 230, + '࠙' => 230, + 'ࠛ' => 230, + 'ࠜ' => 230, + 'ࠝ' => 230, + 'ࠞ' => 230, + 'ࠟ' => 230, + 'ࠠ' => 230, + 'ࠡ' => 230, + 'ࠢ' => 230, + 'ࠣ' => 230, + 'ࠥ' => 230, + 'ࠦ' => 230, + 'ࠧ' => 230, + 'ࠩ' => 230, + 'ࠪ' => 230, + 'ࠫ' => 230, + 'ࠬ' => 230, + '࠭' => 230, + '࡙' => 220, + '࡚' => 220, + '࡛' => 220, + '࣓' => 220, + 'ࣔ' => 230, + 'ࣕ' => 230, + 'ࣖ' => 230, + 'ࣗ' => 230, + 'ࣘ' => 230, + 'ࣙ' => 230, + 'ࣚ' => 230, + 'ࣛ' => 230, + 'ࣜ' => 230, + 'ࣝ' => 230, + 'ࣞ' => 230, + 'ࣟ' => 230, + '࣠' => 230, + '࣡' => 230, + 'ࣣ' => 220, + 'ࣤ' => 230, + 'ࣥ' => 230, + 'ࣦ' => 220, + 'ࣧ' => 230, + 'ࣨ' => 230, + 'ࣩ' => 220, + '࣪' => 230, + '࣫' => 230, + '࣬' => 230, + '࣭' => 220, + '࣮' => 220, + '࣯' => 220, + 'ࣰ' => 27, + 'ࣱ' => 28, + 'ࣲ' => 29, + 'ࣳ' => 230, + 'ࣴ' => 230, + 'ࣵ' => 230, + 'ࣶ' => 220, + 'ࣷ' => 230, + 'ࣸ' => 230, + 'ࣹ' => 220, + 'ࣺ' => 220, + 'ࣻ' => 230, + 'ࣼ' => 230, + 'ࣽ' => 230, + 'ࣾ' => 230, + 'ࣿ' => 230, + '़' => 7, + '्' => 9, + '॑' => 230, + '॒' => 220, + '॓' => 230, + '॔' => 230, + '়' => 7, + '্' => 9, + '৾' => 230, + '਼' => 7, + '੍' => 9, + '઼' => 7, + '્' => 9, + '଼' => 7, + '୍' => 9, + '்' => 9, + '్' => 9, + 'ౕ' => 84, + 'ౖ' => 91, + '಼' => 7, + '್' => 9, + '഻' => 9, + '഼' => 9, + '്' => 9, + '්' => 9, + 'ุ' => 103, + 'ู' => 103, + 'ฺ' => 9, + '่' => 107, + '้' => 107, + '๊' => 107, + '๋' => 107, + 'ຸ' => 118, + 'ູ' => 118, + '຺' => 9, + '່' => 122, + '້' => 122, + '໊' => 122, + '໋' => 122, + '༘' => 220, + '༙' => 220, + '༵' => 220, + '༷' => 220, + '༹' => 216, + 'ཱ' => 129, + 'ི' => 130, + 'ུ' => 132, + 'ེ' => 130, + 'ཻ' => 130, + 'ོ' => 130, + 'ཽ' => 130, + 'ྀ' => 130, + 'ྂ' => 230, + 'ྃ' => 230, + '྄' => 9, + '྆' => 230, + '྇' => 230, + '࿆' => 220, + '့' => 7, + '္' => 9, + '်' => 9, + 'ႍ' => 220, + '፝' => 230, + '፞' => 230, + '፟' => 230, + '᜔' => 9, + '᜴' => 9, + '្' => 9, + '៝' => 230, + 'ᢩ' => 228, + '᤹' => 222, + '᤺' => 230, + '᤻' => 220, + 'ᨗ' => 230, + 'ᨘ' => 220, + '᩠' => 9, + '᩵' => 230, + '᩶' => 230, + '᩷' => 230, + '᩸' => 230, + '᩹' => 230, + '᩺' => 230, + '᩻' => 230, + '᩼' => 230, + '᩿' => 220, + '᪰' => 230, + '᪱' => 230, + '᪲' => 230, + '᪳' => 230, + '᪴' => 230, + '᪵' => 220, + '᪶' => 220, + '᪷' => 220, + '᪸' => 220, + '᪹' => 220, + '᪺' => 220, + '᪻' => 230, + '᪼' => 230, + '᪽' => 220, + 'ᪿ' => 220, + 'ᫀ' => 220, + '᬴' => 7, + '᭄' => 9, + '᭫' => 230, + '᭬' => 220, + '᭭' => 230, + '᭮' => 230, + '᭯' => 230, + '᭰' => 230, + '᭱' => 230, + '᭲' => 230, + '᭳' => 230, + '᮪' => 9, + '᮫' => 9, + '᯦' => 7, + '᯲' => 9, + '᯳' => 9, + '᰷' => 7, + '᳐' => 230, + '᳑' => 230, + '᳒' => 230, + '᳔' => 1, + '᳕' => 220, + '᳖' => 220, + '᳗' => 220, + '᳘' => 220, + '᳙' => 220, + '᳚' => 230, + '᳛' => 230, + '᳜' => 220, + '᳝' => 220, + '᳞' => 220, + '᳟' => 220, + '᳠' => 230, + '᳢' => 1, + '᳣' => 1, + '᳤' => 1, + '᳥' => 1, + '᳦' => 1, + '᳧' => 1, + '᳨' => 1, + '᳭' => 220, + '᳴' => 230, + '᳸' => 230, + '᳹' => 230, + '᷀' => 230, + '᷁' => 230, + '᷂' => 220, + '᷃' => 230, + '᷄' => 230, + '᷅' => 230, + '᷆' => 230, + '᷇' => 230, + '᷈' => 230, + '᷉' => 230, + '᷊' => 220, + '᷋' => 230, + '᷌' => 230, + '᷍' => 234, + '᷎' => 214, + '᷏' => 220, + '᷐' => 202, + '᷑' => 230, + '᷒' => 230, + 'ᷓ' => 230, + 'ᷔ' => 230, + 'ᷕ' => 230, + 'ᷖ' => 230, + 'ᷗ' => 230, + 'ᷘ' => 230, + 'ᷙ' => 230, + 'ᷚ' => 230, + 'ᷛ' => 230, + 'ᷜ' => 230, + 'ᷝ' => 230, + 'ᷞ' => 230, + 'ᷟ' => 230, + 'ᷠ' => 230, + 'ᷡ' => 230, + 'ᷢ' => 230, + 'ᷣ' => 230, + 'ᷤ' => 230, + 'ᷥ' => 230, + 'ᷦ' => 230, + 'ᷧ' => 230, + 'ᷨ' => 230, + 'ᷩ' => 230, + 'ᷪ' => 230, + 'ᷫ' => 230, + 'ᷬ' => 230, + 'ᷭ' => 230, + 'ᷮ' => 230, + 'ᷯ' => 230, + 'ᷰ' => 230, + 'ᷱ' => 230, + 'ᷲ' => 230, + 'ᷳ' => 230, + 'ᷴ' => 230, + '᷵' => 230, + '᷶' => 232, + '᷷' => 228, + '᷸' => 228, + '᷹' => 220, + '᷻' => 230, + '᷼' => 233, + '᷽' => 220, + '᷾' => 230, + '᷿' => 220, + '⃐' => 230, + '⃑' => 230, + '⃒' => 1, + '⃓' => 1, + '⃔' => 230, + '⃕' => 230, + '⃖' => 230, + '⃗' => 230, + '⃘' => 1, + '⃙' => 1, + '⃚' => 1, + '⃛' => 230, + '⃜' => 230, + '⃡' => 230, + '⃥' => 1, + '⃦' => 1, + '⃧' => 230, + '⃨' => 220, + '⃩' => 230, + '⃪' => 1, + '⃫' => 1, + '⃬' => 220, + '⃭' => 220, + '⃮' => 220, + '⃯' => 220, + '⃰' => 230, + '⳯' => 230, + '⳰' => 230, + '⳱' => 230, + '⵿' => 9, + 'ⷠ' => 230, + 'ⷡ' => 230, + 'ⷢ' => 230, + 'ⷣ' => 230, + 'ⷤ' => 230, + 'ⷥ' => 230, + 'ⷦ' => 230, + 'ⷧ' => 230, + 'ⷨ' => 230, + 'ⷩ' => 230, + 'ⷪ' => 230, + 'ⷫ' => 230, + 'ⷬ' => 230, + 'ⷭ' => 230, + 'ⷮ' => 230, + 'ⷯ' => 230, + 'ⷰ' => 230, + 'ⷱ' => 230, + 'ⷲ' => 230, + 'ⷳ' => 230, + 'ⷴ' => 230, + 'ⷵ' => 230, + 'ⷶ' => 230, + 'ⷷ' => 230, + 'ⷸ' => 230, + 'ⷹ' => 230, + 'ⷺ' => 230, + 'ⷻ' => 230, + 'ⷼ' => 230, + 'ⷽ' => 230, + 'ⷾ' => 230, + 'ⷿ' => 230, + '〪' => 218, + '〫' => 228, + '〬' => 232, + '〭' => 222, + '〮' => 224, + '〯' => 224, + '゙' => 8, + '゚' => 8, + '꙯' => 230, + 'ꙴ' => 230, + 'ꙵ' => 230, + 'ꙶ' => 230, + 'ꙷ' => 230, + 'ꙸ' => 230, + 'ꙹ' => 230, + 'ꙺ' => 230, + 'ꙻ' => 230, + '꙼' => 230, + '꙽' => 230, + 'ꚞ' => 230, + 'ꚟ' => 230, + '꛰' => 230, + '꛱' => 230, + '꠆' => 9, + '꠬' => 9, + '꣄' => 9, + '꣠' => 230, + '꣡' => 230, + '꣢' => 230, + '꣣' => 230, + '꣤' => 230, + '꣥' => 230, + '꣦' => 230, + '꣧' => 230, + '꣨' => 230, + '꣩' => 230, + '꣪' => 230, + '꣫' => 230, + '꣬' => 230, + '꣭' => 230, + '꣮' => 230, + '꣯' => 230, + '꣰' => 230, + '꣱' => 230, + '꤫' => 220, + '꤬' => 220, + '꤭' => 220, + '꥓' => 9, + '꦳' => 7, + '꧀' => 9, + 'ꪰ' => 230, + 'ꪲ' => 230, + 'ꪳ' => 230, + 'ꪴ' => 220, + 'ꪷ' => 230, + 'ꪸ' => 230, + 'ꪾ' => 230, + '꪿' => 230, + '꫁' => 230, + '꫶' => 9, + '꯭' => 9, + 'ﬞ' => 26, + '︠' => 230, + '︡' => 230, + '︢' => 230, + '︣' => 230, + '︤' => 230, + '︥' => 230, + '︦' => 230, + '︧' => 220, + '︨' => 220, + '︩' => 220, + '︪' => 220, + '︫' => 220, + '︬' => 220, + '︭' => 220, + '︮' => 230, + '︯' => 230, + '𐇽' => 220, + '𐋠' => 220, + '𐍶' => 230, + '𐍷' => 230, + '𐍸' => 230, + '𐍹' => 230, + '𐍺' => 230, + '𐨍' => 220, + '𐨏' => 230, + '𐨸' => 230, + '𐨹' => 1, + '𐨺' => 220, + '𐨿' => 9, + '𐫥' => 230, + '𐫦' => 220, + '𐴤' => 230, + '𐴥' => 230, + '𐴦' => 230, + '𐴧' => 230, + '𐺫' => 230, + '𐺬' => 230, + '𐽆' => 220, + '𐽇' => 220, + '𐽈' => 230, + '𐽉' => 230, + '𐽊' => 230, + '𐽋' => 220, + '𐽌' => 230, + '𐽍' => 220, + '𐽎' => 220, + '𐽏' => 220, + '𐽐' => 220, + '𑁆' => 9, + '𑁿' => 9, + '𑂹' => 9, + '𑂺' => 7, + '𑄀' => 230, + '𑄁' => 230, + '𑄂' => 230, + '𑄳' => 9, + '𑄴' => 9, + '𑅳' => 7, + '𑇀' => 9, + '𑇊' => 7, + '𑈵' => 9, + '𑈶' => 7, + '𑋩' => 7, + '𑋪' => 9, + '𑌻' => 7, + '𑌼' => 7, + '𑍍' => 9, + '𑍦' => 230, + '𑍧' => 230, + '𑍨' => 230, + '𑍩' => 230, + '𑍪' => 230, + '𑍫' => 230, + '𑍬' => 230, + '𑍰' => 230, + '𑍱' => 230, + '𑍲' => 230, + '𑍳' => 230, + '𑍴' => 230, + '𑑂' => 9, + '𑑆' => 7, + '𑑞' => 230, + '𑓂' => 9, + '𑓃' => 7, + '𑖿' => 9, + '𑗀' => 7, + '𑘿' => 9, + '𑚶' => 9, + '𑚷' => 7, + '𑜫' => 9, + '𑠹' => 9, + '𑠺' => 7, + '𑤽' => 9, + '𑤾' => 9, + '𑥃' => 7, + '𑧠' => 9, + '𑨴' => 9, + '𑩇' => 9, + '𑪙' => 9, + '𑰿' => 9, + '𑵂' => 7, + '𑵄' => 9, + '𑵅' => 9, + '𑶗' => 9, + '𖫰' => 1, + '𖫱' => 1, + '𖫲' => 1, + '𖫳' => 1, + '𖫴' => 1, + '𖬰' => 230, + '𖬱' => 230, + '𖬲' => 230, + '𖬳' => 230, + '𖬴' => 230, + '𖬵' => 230, + '𖬶' => 230, + '𖿰' => 6, + '𖿱' => 6, + '𛲞' => 1, + '𝅥' => 216, + '𝅦' => 216, + '𝅧' => 1, + '𝅨' => 1, + '𝅩' => 1, + '𝅭' => 226, + '𝅮' => 216, + '𝅯' => 216, + '𝅰' => 216, + '𝅱' => 216, + '𝅲' => 216, + '𝅻' => 220, + '𝅼' => 220, + '𝅽' => 220, + '𝅾' => 220, + '𝅿' => 220, + '𝆀' => 220, + '𝆁' => 220, + '𝆂' => 220, + '𝆅' => 230, + '𝆆' => 230, + '𝆇' => 230, + '𝆈' => 230, + '𝆉' => 230, + '𝆊' => 220, + '𝆋' => 220, + '𝆪' => 230, + '𝆫' => 230, + '𝆬' => 230, + '𝆭' => 230, + '𝉂' => 230, + '𝉃' => 230, + '𝉄' => 230, + '𞀀' => 230, + '𞀁' => 230, + '𞀂' => 230, + '𞀃' => 230, + '𞀄' => 230, + '𞀅' => 230, + '𞀆' => 230, + '𞀈' => 230, + '𞀉' => 230, + '𞀊' => 230, + '𞀋' => 230, + '𞀌' => 230, + '𞀍' => 230, + '𞀎' => 230, + '𞀏' => 230, + '𞀐' => 230, + '𞀑' => 230, + '𞀒' => 230, + '𞀓' => 230, + '𞀔' => 230, + '𞀕' => 230, + '𞀖' => 230, + '𞀗' => 230, + '𞀘' => 230, + '𞀛' => 230, + '𞀜' => 230, + '𞀝' => 230, + '𞀞' => 230, + '𞀟' => 230, + '𞀠' => 230, + '𞀡' => 230, + '𞀣' => 230, + '𞀤' => 230, + '𞀦' => 230, + '𞀧' => 230, + '𞀨' => 230, + '𞀩' => 230, + '𞀪' => 230, + '𞄰' => 230, + '𞄱' => 230, + '𞄲' => 230, + '𞄳' => 230, + '𞄴' => 230, + '𞄵' => 230, + '𞄶' => 230, + '𞋬' => 230, + '𞋭' => 230, + '𞋮' => 230, + '𞋯' => 230, + '𞣐' => 220, + '𞣑' => 220, + '𞣒' => 220, + '𞣓' => 220, + '𞣔' => 220, + '𞣕' => 220, + '𞣖' => 220, + '𞥄' => 230, + '𞥅' => 230, + '𞥆' => 230, + '𞥇' => 230, + '𞥈' => 230, + '𞥉' => 230, + '𞥊' => 7, +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php new file mode 100644 index 0000000..1574902 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php @@ -0,0 +1,3695 @@ + ' ', + '¨' => ' ̈', + 'ª' => 'a', + '¯' => ' ̄', + '²' => '2', + '³' => '3', + '´' => ' ́', + 'µ' => 'μ', + '¸' => ' ̧', + '¹' => '1', + 'º' => 'o', + '¼' => '1⁄4', + '½' => '1⁄2', + '¾' => '3⁄4', + 'IJ' => 'IJ', + 'ij' => 'ij', + 'Ŀ' => 'L·', + 'ŀ' => 'l·', + 'ʼn' => 'ʼn', + 'ſ' => 's', + 'DŽ' => 'DŽ', + 'Dž' => 'Dž', + 'dž' => 'dž', + 'LJ' => 'LJ', + 'Lj' => 'Lj', + 'lj' => 'lj', + 'NJ' => 'NJ', + 'Nj' => 'Nj', + 'nj' => 'nj', + 'DZ' => 'DZ', + 'Dz' => 'Dz', + 'dz' => 'dz', + 'ʰ' => 'h', + 'ʱ' => 'ɦ', + 'ʲ' => 'j', + 'ʳ' => 'r', + 'ʴ' => 'ɹ', + 'ʵ' => 'ɻ', + 'ʶ' => 'ʁ', + 'ʷ' => 'w', + 'ʸ' => 'y', + '˘' => ' ̆', + '˙' => ' ̇', + '˚' => ' ̊', + '˛' => ' ̨', + '˜' => ' ̃', + '˝' => ' ̋', + 'ˠ' => 'ɣ', + 'ˡ' => 'l', + 'ˢ' => 's', + 'ˣ' => 'x', + 'ˤ' => 'ʕ', + 'ͺ' => ' ͅ', + '΄' => ' ́', + '΅' => ' ̈́', + 'ϐ' => 'β', + 'ϑ' => 'θ', + 'ϒ' => 'Υ', + 'ϓ' => 'Ύ', + 'ϔ' => 'Ϋ', + 'ϕ' => 'φ', + 'ϖ' => 'π', + 'ϰ' => 'κ', + 'ϱ' => 'ρ', + 'ϲ' => 'ς', + 'ϴ' => 'Θ', + 'ϵ' => 'ε', + 'Ϲ' => 'Σ', + 'և' => 'եւ', + 'ٵ' => 'اٴ', + 'ٶ' => 'وٴ', + 'ٷ' => 'ۇٴ', + 'ٸ' => 'يٴ', + 'ำ' => 'ํา', + 'ຳ' => 'ໍາ', + 'ໜ' => 'ຫນ', + 'ໝ' => 'ຫມ', + '༌' => '་', + 'ཷ' => 'ྲཱྀ', + 'ཹ' => 'ླཱྀ', + 'ჼ' => 'ნ', + 'ᴬ' => 'A', + 'ᴭ' => 'Æ', + 'ᴮ' => 'B', + 'ᴰ' => 'D', + 'ᴱ' => 'E', + 'ᴲ' => 'Ǝ', + 'ᴳ' => 'G', + 'ᴴ' => 'H', + 'ᴵ' => 'I', + 'ᴶ' => 'J', + 'ᴷ' => 'K', + 'ᴸ' => 'L', + 'ᴹ' => 'M', + 'ᴺ' => 'N', + 'ᴼ' => 'O', + 'ᴽ' => 'Ȣ', + 'ᴾ' => 'P', + 'ᴿ' => 'R', + 'ᵀ' => 'T', + 'ᵁ' => 'U', + 'ᵂ' => 'W', + 'ᵃ' => 'a', + 'ᵄ' => 'ɐ', + 'ᵅ' => 'ɑ', + 'ᵆ' => 'ᴂ', + 'ᵇ' => 'b', + 'ᵈ' => 'd', + 'ᵉ' => 'e', + 'ᵊ' => 'ə', + 'ᵋ' => 'ɛ', + 'ᵌ' => 'ɜ', + 'ᵍ' => 'g', + 'ᵏ' => 'k', + 'ᵐ' => 'm', + 'ᵑ' => 'ŋ', + 'ᵒ' => 'o', + 'ᵓ' => 'ɔ', + 'ᵔ' => 'ᴖ', + 'ᵕ' => 'ᴗ', + 'ᵖ' => 'p', + 'ᵗ' => 't', + 'ᵘ' => 'u', + 'ᵙ' => 'ᴝ', + 'ᵚ' => 'ɯ', + 'ᵛ' => 'v', + 'ᵜ' => 'ᴥ', + 'ᵝ' => 'β', + 'ᵞ' => 'γ', + 'ᵟ' => 'δ', + 'ᵠ' => 'φ', + 'ᵡ' => 'χ', + 'ᵢ' => 'i', + 'ᵣ' => 'r', + 'ᵤ' => 'u', + 'ᵥ' => 'v', + 'ᵦ' => 'β', + 'ᵧ' => 'γ', + 'ᵨ' => 'ρ', + 'ᵩ' => 'φ', + 'ᵪ' => 'χ', + 'ᵸ' => 'н', + 'ᶛ' => 'ɒ', + 'ᶜ' => 'c', + 'ᶝ' => 'ɕ', + 'ᶞ' => 'ð', + 'ᶟ' => 'ɜ', + 'ᶠ' => 'f', + 'ᶡ' => 'ɟ', + 'ᶢ' => 'ɡ', + 'ᶣ' => 'ɥ', + 'ᶤ' => 'ɨ', + 'ᶥ' => 'ɩ', + 'ᶦ' => 'ɪ', + 'ᶧ' => 'ᵻ', + 'ᶨ' => 'ʝ', + 'ᶩ' => 'ɭ', + 'ᶪ' => 'ᶅ', + 'ᶫ' => 'ʟ', + 'ᶬ' => 'ɱ', + 'ᶭ' => 'ɰ', + 'ᶮ' => 'ɲ', + 'ᶯ' => 'ɳ', + 'ᶰ' => 'ɴ', + 'ᶱ' => 'ɵ', + 'ᶲ' => 'ɸ', + 'ᶳ' => 'ʂ', + 'ᶴ' => 'ʃ', + 'ᶵ' => 'ƫ', + 'ᶶ' => 'ʉ', + 'ᶷ' => 'ʊ', + 'ᶸ' => 'ᴜ', + 'ᶹ' => 'ʋ', + 'ᶺ' => 'ʌ', + 'ᶻ' => 'z', + 'ᶼ' => 'ʐ', + 'ᶽ' => 'ʑ', + 'ᶾ' => 'ʒ', + 'ᶿ' => 'θ', + 'ẚ' => 'aʾ', + 'ẛ' => 'ṡ', + '᾽' => ' ̓', + '᾿' => ' ̓', + '῀' => ' ͂', + '῁' => ' ̈͂', + '῍' => ' ̓̀', + '῎' => ' ̓́', + '῏' => ' ̓͂', + '῝' => ' ̔̀', + '῞' => ' ̔́', + '῟' => ' ̔͂', + '῭' => ' ̈̀', + '΅' => ' ̈́', + '´' => ' ́', + '῾' => ' ̔', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + '‑' => '‐', + '‗' => ' ̳', + '․' => '.', + '‥' => '..', + '…' => '...', + ' ' => ' ', + '″' => '′′', + '‴' => '′′′', + '‶' => '‵‵', + '‷' => '‵‵‵', + '‼' => '!!', + '‾' => ' ̅', + '⁇' => '??', + '⁈' => '?!', + '⁉' => '!?', + '⁗' => '′′′′', + ' ' => ' ', + '⁰' => '0', + 'ⁱ' => 'i', + '⁴' => '4', + '⁵' => '5', + '⁶' => '6', + '⁷' => '7', + '⁸' => '8', + '⁹' => '9', + '⁺' => '+', + '⁻' => '−', + '⁼' => '=', + '⁽' => '(', + '⁾' => ')', + 'ⁿ' => 'n', + '₀' => '0', + '₁' => '1', + '₂' => '2', + '₃' => '3', + '₄' => '4', + '₅' => '5', + '₆' => '6', + '₇' => '7', + '₈' => '8', + '₉' => '9', + '₊' => '+', + '₋' => '−', + '₌' => '=', + '₍' => '(', + '₎' => ')', + 'ₐ' => 'a', + 'ₑ' => 'e', + 'ₒ' => 'o', + 'ₓ' => 'x', + 'ₔ' => 'ə', + 'ₕ' => 'h', + 'ₖ' => 'k', + 'ₗ' => 'l', + 'ₘ' => 'm', + 'ₙ' => 'n', + 'ₚ' => 'p', + 'ₛ' => 's', + 'ₜ' => 't', + '₨' => 'Rs', + '℀' => 'a/c', + '℁' => 'a/s', + 'ℂ' => 'C', + '℃' => '°C', + '℅' => 'c/o', + '℆' => 'c/u', + 'ℇ' => 'Ɛ', + '℉' => '°F', + 'ℊ' => 'g', + 'ℋ' => 'H', + 'ℌ' => 'H', + 'ℍ' => 'H', + 'ℎ' => 'h', + 'ℏ' => 'ħ', + 'ℐ' => 'I', + 'ℑ' => 'I', + 'ℒ' => 'L', + 'ℓ' => 'l', + 'ℕ' => 'N', + '№' => 'No', + 'ℙ' => 'P', + 'ℚ' => 'Q', + 'ℛ' => 'R', + 'ℜ' => 'R', + 'ℝ' => 'R', + '℠' => 'SM', + '℡' => 'TEL', + '™' => 'TM', + 'ℤ' => 'Z', + 'ℨ' => 'Z', + 'ℬ' => 'B', + 'ℭ' => 'C', + 'ℯ' => 'e', + 'ℰ' => 'E', + 'ℱ' => 'F', + 'ℳ' => 'M', + 'ℴ' => 'o', + 'ℵ' => 'א', + 'ℶ' => 'ב', + 'ℷ' => 'ג', + 'ℸ' => 'ד', + 'ℹ' => 'i', + '℻' => 'FAX', + 'ℼ' => 'π', + 'ℽ' => 'γ', + 'ℾ' => 'Γ', + 'ℿ' => 'Π', + '⅀' => '∑', + 'ⅅ' => 'D', + 'ⅆ' => 'd', + 'ⅇ' => 'e', + 'ⅈ' => 'i', + 'ⅉ' => 'j', + '⅐' => '1⁄7', + '⅑' => '1⁄9', + '⅒' => '1⁄10', + '⅓' => '1⁄3', + '⅔' => '2⁄3', + '⅕' => '1⁄5', + '⅖' => '2⁄5', + '⅗' => '3⁄5', + '⅘' => '4⁄5', + '⅙' => '1⁄6', + '⅚' => '5⁄6', + '⅛' => '1⁄8', + '⅜' => '3⁄8', + '⅝' => '5⁄8', + '⅞' => '7⁄8', + '⅟' => '1⁄', + 'Ⅰ' => 'I', + 'Ⅱ' => 'II', + 'Ⅲ' => 'III', + 'Ⅳ' => 'IV', + 'Ⅴ' => 'V', + 'Ⅵ' => 'VI', + 'Ⅶ' => 'VII', + 'Ⅷ' => 'VIII', + 'Ⅸ' => 'IX', + 'Ⅹ' => 'X', + 'Ⅺ' => 'XI', + 'Ⅻ' => 'XII', + 'Ⅼ' => 'L', + 'Ⅽ' => 'C', + 'Ⅾ' => 'D', + 'Ⅿ' => 'M', + 'ⅰ' => 'i', + 'ⅱ' => 'ii', + 'ⅲ' => 'iii', + 'ⅳ' => 'iv', + 'ⅴ' => 'v', + 'ⅵ' => 'vi', + 'ⅶ' => 'vii', + 'ⅷ' => 'viii', + 'ⅸ' => 'ix', + 'ⅹ' => 'x', + 'ⅺ' => 'xi', + 'ⅻ' => 'xii', + 'ⅼ' => 'l', + 'ⅽ' => 'c', + 'ⅾ' => 'd', + 'ⅿ' => 'm', + '↉' => '0⁄3', + '∬' => '∫∫', + '∭' => '∫∫∫', + '∯' => '∮∮', + '∰' => '∮∮∮', + '①' => '1', + '②' => '2', + '③' => '3', + '④' => '4', + '⑤' => '5', + '⑥' => '6', + '⑦' => '7', + '⑧' => '8', + '⑨' => '9', + '⑩' => '10', + '⑪' => '11', + '⑫' => '12', + '⑬' => '13', + '⑭' => '14', + '⑮' => '15', + '⑯' => '16', + '⑰' => '17', + '⑱' => '18', + '⑲' => '19', + '⑳' => '20', + '⑴' => '(1)', + '⑵' => '(2)', + '⑶' => '(3)', + '⑷' => '(4)', + '⑸' => '(5)', + '⑹' => '(6)', + '⑺' => '(7)', + '⑻' => '(8)', + '⑼' => '(9)', + '⑽' => '(10)', + '⑾' => '(11)', + '⑿' => '(12)', + '⒀' => '(13)', + '⒁' => '(14)', + '⒂' => '(15)', + '⒃' => '(16)', + '⒄' => '(17)', + '⒅' => '(18)', + '⒆' => '(19)', + '⒇' => '(20)', + '⒈' => '1.', + '⒉' => '2.', + '⒊' => '3.', + '⒋' => '4.', + '⒌' => '5.', + '⒍' => '6.', + '⒎' => '7.', + '⒏' => '8.', + '⒐' => '9.', + '⒑' => '10.', + '⒒' => '11.', + '⒓' => '12.', + '⒔' => '13.', + '⒕' => '14.', + '⒖' => '15.', + '⒗' => '16.', + '⒘' => '17.', + '⒙' => '18.', + '⒚' => '19.', + '⒛' => '20.', + '⒜' => '(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)', + 'Ⓐ' => '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', + 'ⓐ' => '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', + '⓪' => '0', + '⨌' => '∫∫∫∫', + '⩴' => '::=', + '⩵' => '==', + '⩶' => '===', + 'ⱼ' => 'j', + 'ⱽ' => 'V', + 'ⵯ' => 'ⵡ', + '⺟' => '母', + '⻳' => '龟', + '⼀' => '一', + '⼁' => '丨', + '⼂' => '丶', + '⼃' => '丿', + '⼄' => '乙', + '⼅' => '亅', + '⼆' => '二', + '⼇' => '亠', + '⼈' => '人', + '⼉' => '儿', + '⼊' => '入', + '⼋' => '八', + '⼌' => '冂', + '⼍' => '冖', + '⼎' => '冫', + '⼏' => '几', + '⼐' => '凵', + '⼑' => '刀', + '⼒' => '力', + '⼓' => '勹', + '⼔' => '匕', + '⼕' => '匚', + '⼖' => '匸', + '⼗' => '十', + '⼘' => '卜', + '⼙' => '卩', + '⼚' => '厂', + '⼛' => '厶', + '⼜' => '又', + '⼝' => '口', + '⼞' => '囗', + '⼟' => '土', + '⼠' => '士', + '⼡' => '夂', + '⼢' => '夊', + '⼣' => '夕', + '⼤' => '大', + '⼥' => '女', + '⼦' => '子', + '⼧' => '宀', + '⼨' => '寸', + '⼩' => '小', + '⼪' => '尢', + '⼫' => '尸', + '⼬' => '屮', + '⼭' => '山', + '⼮' => '巛', + '⼯' => '工', + '⼰' => '己', + '⼱' => '巾', + '⼲' => '干', + '⼳' => '幺', + '⼴' => '广', + '⼵' => '廴', + '⼶' => '廾', + '⼷' => '弋', + '⼸' => '弓', + '⼹' => '彐', + '⼺' => '彡', + '⼻' => '彳', + '⼼' => '心', + '⼽' => '戈', + '⼾' => '戶', + '⼿' => '手', + '⽀' => '支', + '⽁' => '攴', + '⽂' => '文', + '⽃' => '斗', + '⽄' => '斤', + '⽅' => '方', + '⽆' => '无', + '⽇' => '日', + '⽈' => '曰', + '⽉' => '月', + '⽊' => '木', + '⽋' => '欠', + '⽌' => '止', + '⽍' => '歹', + '⽎' => '殳', + '⽏' => '毋', + '⽐' => '比', + '⽑' => '毛', + '⽒' => '氏', + '⽓' => '气', + '⽔' => '水', + '⽕' => '火', + '⽖' => '爪', + '⽗' => '父', + '⽘' => '爻', + '⽙' => '爿', + '⽚' => '片', + '⽛' => '牙', + '⽜' => '牛', + '⽝' => '犬', + '⽞' => '玄', + '⽟' => '玉', + '⽠' => '瓜', + '⽡' => '瓦', + '⽢' => '甘', + '⽣' => '生', + '⽤' => '用', + '⽥' => '田', + '⽦' => '疋', + '⽧' => '疒', + '⽨' => '癶', + '⽩' => '白', + '⽪' => '皮', + '⽫' => '皿', + '⽬' => '目', + '⽭' => '矛', + '⽮' => '矢', + '⽯' => '石', + '⽰' => '示', + '⽱' => '禸', + '⽲' => '禾', + '⽳' => '穴', + '⽴' => '立', + '⽵' => '竹', + '⽶' => '米', + '⽷' => '糸', + '⽸' => '缶', + '⽹' => '网', + '⽺' => '羊', + '⽻' => '羽', + '⽼' => '老', + '⽽' => '而', + '⽾' => '耒', + '⽿' => '耳', + '⾀' => '聿', + '⾁' => '肉', + '⾂' => '臣', + '⾃' => '自', + '⾄' => '至', + '⾅' => '臼', + '⾆' => '舌', + '⾇' => '舛', + '⾈' => '舟', + '⾉' => '艮', + '⾊' => '色', + '⾋' => '艸', + '⾌' => '虍', + '⾍' => '虫', + '⾎' => '血', + '⾏' => '行', + '⾐' => '衣', + '⾑' => '襾', + '⾒' => '見', + '⾓' => '角', + '⾔' => '言', + '⾕' => '谷', + '⾖' => '豆', + '⾗' => '豕', + '⾘' => '豸', + '⾙' => '貝', + '⾚' => '赤', + '⾛' => '走', + '⾜' => '足', + '⾝' => '身', + '⾞' => '車', + '⾟' => '辛', + '⾠' => '辰', + '⾡' => '辵', + '⾢' => '邑', + '⾣' => '酉', + '⾤' => '釆', + '⾥' => '里', + '⾦' => '金', + '⾧' => '長', + '⾨' => '門', + '⾩' => '阜', + '⾪' => '隶', + '⾫' => '隹', + '⾬' => '雨', + '⾭' => '靑', + '⾮' => '非', + '⾯' => '面', + '⾰' => '革', + '⾱' => '韋', + '⾲' => '韭', + '⾳' => '音', + '⾴' => '頁', + '⾵' => '風', + '⾶' => '飛', + '⾷' => '食', + '⾸' => '首', + '⾹' => '香', + '⾺' => '馬', + '⾻' => '骨', + '⾼' => '高', + '⾽' => '髟', + '⾾' => '鬥', + '⾿' => '鬯', + '⿀' => '鬲', + '⿁' => '鬼', + '⿂' => '魚', + '⿃' => '鳥', + '⿄' => '鹵', + '⿅' => '鹿', + '⿆' => '麥', + '⿇' => '麻', + '⿈' => '黃', + '⿉' => '黍', + '⿊' => '黑', + '⿋' => '黹', + '⿌' => '黽', + '⿍' => '鼎', + '⿎' => '鼓', + '⿏' => '鼠', + '⿐' => '鼻', + '⿑' => '齊', + '⿒' => '齒', + '⿓' => '龍', + '⿔' => '龜', + '⿕' => '龠', + ' ' => ' ', + '〶' => '〒', + '〸' => '十', + '〹' => '卄', + '〺' => '卅', + '゛' => ' ゙', + '゜' => ' ゚', + 'ゟ' => 'より', + 'ヿ' => 'コト', + 'ㄱ' => 'ᄀ', + 'ㄲ' => 'ᄁ', + 'ㄳ' => 'ᆪ', + 'ㄴ' => 'ᄂ', + 'ㄵ' => 'ᆬ', + 'ㄶ' => 'ᆭ', + 'ㄷ' => 'ᄃ', + 'ㄸ' => 'ᄄ', + 'ㄹ' => 'ᄅ', + 'ㄺ' => 'ᆰ', + 'ㄻ' => 'ᆱ', + 'ㄼ' => 'ᆲ', + 'ㄽ' => 'ᆳ', + 'ㄾ' => 'ᆴ', + 'ㄿ' => 'ᆵ', + 'ㅀ' => 'ᄚ', + 'ㅁ' => 'ᄆ', + 'ㅂ' => 'ᄇ', + 'ㅃ' => 'ᄈ', + 'ㅄ' => 'ᄡ', + 'ㅅ' => 'ᄉ', + 'ㅆ' => 'ᄊ', + 'ㅇ' => 'ᄋ', + 'ㅈ' => 'ᄌ', + 'ㅉ' => 'ᄍ', + 'ㅊ' => 'ᄎ', + 'ㅋ' => 'ᄏ', + 'ㅌ' => 'ᄐ', + 'ㅍ' => 'ᄑ', + 'ㅎ' => 'ᄒ', + 'ㅏ' => 'ᅡ', + 'ㅐ' => 'ᅢ', + 'ㅑ' => 'ᅣ', + 'ㅒ' => 'ᅤ', + 'ㅓ' => 'ᅥ', + 'ㅔ' => 'ᅦ', + 'ㅕ' => 'ᅧ', + 'ㅖ' => 'ᅨ', + 'ㅗ' => 'ᅩ', + 'ㅘ' => 'ᅪ', + 'ㅙ' => 'ᅫ', + 'ㅚ' => 'ᅬ', + 'ㅛ' => 'ᅭ', + 'ㅜ' => 'ᅮ', + 'ㅝ' => 'ᅯ', + 'ㅞ' => 'ᅰ', + 'ㅟ' => 'ᅱ', + 'ㅠ' => 'ᅲ', + 'ㅡ' => 'ᅳ', + 'ㅢ' => 'ᅴ', + 'ㅣ' => 'ᅵ', + 'ㅤ' => 'ᅠ', + 'ㅥ' => 'ᄔ', + 'ㅦ' => 'ᄕ', + 'ㅧ' => 'ᇇ', + 'ㅨ' => 'ᇈ', + 'ㅩ' => 'ᇌ', + 'ㅪ' => 'ᇎ', + 'ㅫ' => 'ᇓ', + 'ㅬ' => 'ᇗ', + 'ㅭ' => 'ᇙ', + 'ㅮ' => 'ᄜ', + 'ㅯ' => 'ᇝ', + 'ㅰ' => 'ᇟ', + 'ㅱ' => 'ᄝ', + 'ㅲ' => 'ᄞ', + 'ㅳ' => 'ᄠ', + 'ㅴ' => 'ᄢ', + 'ㅵ' => 'ᄣ', + 'ㅶ' => 'ᄧ', + 'ㅷ' => 'ᄩ', + 'ㅸ' => 'ᄫ', + 'ㅹ' => 'ᄬ', + 'ㅺ' => 'ᄭ', + 'ㅻ' => 'ᄮ', + 'ㅼ' => 'ᄯ', + 'ㅽ' => 'ᄲ', + 'ㅾ' => 'ᄶ', + 'ㅿ' => 'ᅀ', + 'ㆀ' => 'ᅇ', + 'ㆁ' => 'ᅌ', + 'ㆂ' => 'ᇱ', + 'ㆃ' => 'ᇲ', + 'ㆄ' => 'ᅗ', + 'ㆅ' => 'ᅘ', + 'ㆆ' => 'ᅙ', + 'ㆇ' => 'ᆄ', + 'ㆈ' => 'ᆅ', + 'ㆉ' => 'ᆈ', + 'ㆊ' => 'ᆑ', + 'ㆋ' => 'ᆒ', + 'ㆌ' => 'ᆔ', + 'ㆍ' => 'ᆞ', + 'ㆎ' => 'ᆡ', + '㆒' => '一', + '㆓' => '二', + '㆔' => '三', + '㆕' => '四', + '㆖' => '上', + '㆗' => '中', + '㆘' => '下', + '㆙' => '甲', + '㆚' => '乙', + '㆛' => '丙', + '㆜' => '丁', + '㆝' => '天', + '㆞' => '地', + '㆟' => '人', + '㈀' => '(ᄀ)', + '㈁' => '(ᄂ)', + '㈂' => '(ᄃ)', + '㈃' => '(ᄅ)', + '㈄' => '(ᄆ)', + '㈅' => '(ᄇ)', + '㈆' => '(ᄉ)', + '㈇' => '(ᄋ)', + '㈈' => '(ᄌ)', + '㈉' => '(ᄎ)', + '㈊' => '(ᄏ)', + '㈋' => '(ᄐ)', + '㈌' => '(ᄑ)', + '㈍' => '(ᄒ)', + '㈎' => '(가)', + '㈏' => '(나)', + '㈐' => '(다)', + '㈑' => '(라)', + '㈒' => '(마)', + '㈓' => '(바)', + '㈔' => '(사)', + '㈕' => '(아)', + '㈖' => '(자)', + '㈗' => '(차)', + '㈘' => '(카)', + '㈙' => '(타)', + '㈚' => '(파)', + '㈛' => '(하)', + '㈜' => '(주)', + '㈝' => '(오전)', + '㈞' => '(오후)', + '㈠' => '(一)', + '㈡' => '(二)', + '㈢' => '(三)', + '㈣' => '(四)', + '㈤' => '(五)', + '㈥' => '(六)', + '㈦' => '(七)', + '㈧' => '(八)', + '㈨' => '(九)', + '㈩' => '(十)', + '㈪' => '(月)', + '㈫' => '(火)', + '㈬' => '(水)', + '㈭' => '(木)', + '㈮' => '(金)', + '㈯' => '(土)', + '㈰' => '(日)', + '㈱' => '(株)', + '㈲' => '(有)', + '㈳' => '(社)', + '㈴' => '(名)', + '㈵' => '(特)', + '㈶' => '(財)', + '㈷' => '(祝)', + '㈸' => '(労)', + '㈹' => '(代)', + '㈺' => '(呼)', + '㈻' => '(学)', + '㈼' => '(監)', + '㈽' => '(企)', + '㈾' => '(資)', + '㈿' => '(協)', + '㉀' => '(祭)', + '㉁' => '(休)', + '㉂' => '(自)', + '㉃' => '(至)', + '㉄' => '問', + '㉅' => '幼', + '㉆' => '文', + '㉇' => '箏', + '㉐' => 'PTE', + '㉑' => '21', + '㉒' => '22', + '㉓' => '23', + '㉔' => '24', + '㉕' => '25', + '㉖' => '26', + '㉗' => '27', + '㉘' => '28', + '㉙' => '29', + '㉚' => '30', + '㉛' => '31', + '㉜' => '32', + '㉝' => '33', + '㉞' => '34', + '㉟' => '35', + '㉠' => 'ᄀ', + '㉡' => 'ᄂ', + '㉢' => 'ᄃ', + '㉣' => 'ᄅ', + '㉤' => 'ᄆ', + '㉥' => 'ᄇ', + '㉦' => 'ᄉ', + '㉧' => 'ᄋ', + '㉨' => 'ᄌ', + '㉩' => 'ᄎ', + '㉪' => 'ᄏ', + '㉫' => 'ᄐ', + '㉬' => 'ᄑ', + '㉭' => 'ᄒ', + '㉮' => '가', + '㉯' => '나', + '㉰' => '다', + '㉱' => '라', + '㉲' => '마', + '㉳' => '바', + '㉴' => '사', + '㉵' => '아', + '㉶' => '자', + '㉷' => '차', + '㉸' => '카', + '㉹' => '타', + '㉺' => '파', + '㉻' => '하', + '㉼' => '참고', + '㉽' => '주의', + '㉾' => '우', + '㊀' => '一', + '㊁' => '二', + '㊂' => '三', + '㊃' => '四', + '㊄' => '五', + '㊅' => '六', + '㊆' => '七', + '㊇' => '八', + '㊈' => '九', + '㊉' => '十', + '㊊' => '月', + '㊋' => '火', + '㊌' => '水', + '㊍' => '木', + '㊎' => '金', + '㊏' => '土', + '㊐' => '日', + '㊑' => '株', + '㊒' => '有', + '㊓' => '社', + '㊔' => '名', + '㊕' => '特', + '㊖' => '財', + '㊗' => '祝', + '㊘' => '労', + '㊙' => '秘', + '㊚' => '男', + '㊛' => '女', + '㊜' => '適', + '㊝' => '優', + '㊞' => '印', + '㊟' => '注', + '㊠' => '項', + '㊡' => '休', + '㊢' => '写', + '㊣' => '正', + '㊤' => '上', + '㊥' => '中', + '㊦' => '下', + '㊧' => '左', + '㊨' => '右', + '㊩' => '医', + '㊪' => '宗', + '㊫' => '学', + '㊬' => '監', + '㊭' => '企', + '㊮' => '資', + '㊯' => '協', + '㊰' => '夜', + '㊱' => '36', + '㊲' => '37', + '㊳' => '38', + '㊴' => '39', + '㊵' => '40', + '㊶' => '41', + '㊷' => '42', + '㊸' => '43', + '㊹' => '44', + '㊺' => '45', + '㊻' => '46', + '㊼' => '47', + '㊽' => '48', + '㊾' => '49', + '㊿' => '50', + '㋀' => '1月', + '㋁' => '2月', + '㋂' => '3月', + '㋃' => '4月', + '㋄' => '5月', + '㋅' => '6月', + '㋆' => '7月', + '㋇' => '8月', + '㋈' => '9月', + '㋉' => '10月', + '㋊' => '11月', + '㋋' => '12月', + '㋌' => 'Hg', + '㋍' => 'erg', + '㋎' => 'eV', + '㋏' => 'LTD', + '㋐' => 'ア', + '㋑' => 'イ', + '㋒' => 'ウ', + '㋓' => 'エ', + '㋔' => 'オ', + '㋕' => 'カ', + '㋖' => 'キ', + '㋗' => 'ク', + '㋘' => 'ケ', + '㋙' => 'コ', + '㋚' => 'サ', + '㋛' => 'シ', + '㋜' => 'ス', + '㋝' => 'セ', + '㋞' => 'ソ', + '㋟' => 'タ', + '㋠' => 'チ', + '㋡' => 'ツ', + '㋢' => 'テ', + '㋣' => 'ト', + '㋤' => 'ナ', + '㋥' => 'ニ', + '㋦' => 'ヌ', + '㋧' => 'ネ', + '㋨' => 'ノ', + '㋩' => 'ハ', + '㋪' => 'ヒ', + '㋫' => 'フ', + '㋬' => 'ヘ', + '㋭' => 'ホ', + '㋮' => 'マ', + '㋯' => 'ミ', + '㋰' => 'ム', + '㋱' => 'メ', + '㋲' => 'モ', + '㋳' => 'ヤ', + '㋴' => 'ユ', + '㋵' => 'ヨ', + '㋶' => 'ラ', + '㋷' => 'リ', + '㋸' => 'ル', + '㋹' => 'レ', + '㋺' => 'ロ', + '㋻' => 'ワ', + '㋼' => 'ヰ', + '㋽' => 'ヱ', + '㋾' => 'ヲ', + '㋿' => '令和', + '㌀' => 'アパート', + '㌁' => 'アルファ', + '㌂' => 'アンペア', + '㌃' => 'アール', + '㌄' => 'イニング', + '㌅' => 'インチ', + '㌆' => 'ウォン', + '㌇' => 'エスクード', + '㌈' => 'エーカー', + '㌉' => 'オンス', + '㌊' => 'オーム', + '㌋' => 'カイリ', + '㌌' => 'カラット', + '㌍' => 'カロリー', + '㌎' => 'ガロン', + '㌏' => 'ガンマ', + '㌐' => 'ギガ', + '㌑' => 'ギニー', + '㌒' => 'キュリー', + '㌓' => 'ギルダー', + '㌔' => 'キロ', + '㌕' => 'キログラム', + '㌖' => 'キロメートル', + '㌗' => 'キロワット', + '㌘' => 'グラム', + '㌙' => 'グラムトン', + '㌚' => 'クルゼイロ', + '㌛' => 'クローネ', + '㌜' => 'ケース', + '㌝' => 'コルナ', + '㌞' => 'コーポ', + '㌟' => 'サイクル', + '㌠' => 'サンチーム', + '㌡' => 'シリング', + '㌢' => 'センチ', + '㌣' => 'セント', + '㌤' => 'ダース', + '㌥' => 'デシ', + '㌦' => 'ドル', + '㌧' => 'トン', + '㌨' => 'ナノ', + '㌩' => 'ノット', + '㌪' => 'ハイツ', + '㌫' => 'パーセント', + '㌬' => 'パーツ', + '㌭' => 'バーレル', + '㌮' => 'ピアストル', + '㌯' => 'ピクル', + '㌰' => 'ピコ', + '㌱' => 'ビル', + '㌲' => 'ファラッド', + '㌳' => 'フィート', + '㌴' => 'ブッシェル', + '㌵' => 'フラン', + '㌶' => 'ヘクタール', + '㌷' => 'ペソ', + '㌸' => 'ペニヒ', + '㌹' => 'ヘルツ', + '㌺' => 'ペンス', + '㌻' => 'ページ', + '㌼' => 'ベータ', + '㌽' => 'ポイント', + '㌾' => 'ボルト', + '㌿' => 'ホン', + '㍀' => 'ポンド', + '㍁' => 'ホール', + '㍂' => 'ホーン', + '㍃' => 'マイクロ', + '㍄' => 'マイル', + '㍅' => 'マッハ', + '㍆' => 'マルク', + '㍇' => 'マンション', + '㍈' => 'ミクロン', + '㍉' => 'ミリ', + '㍊' => 'ミリバール', + '㍋' => 'メガ', + '㍌' => 'メガトン', + '㍍' => 'メートル', + '㍎' => 'ヤード', + '㍏' => 'ヤール', + '㍐' => 'ユアン', + '㍑' => 'リットル', + '㍒' => 'リラ', + '㍓' => 'ルピー', + '㍔' => 'ルーブル', + '㍕' => 'レム', + '㍖' => 'レントゲン', + '㍗' => 'ワット', + '㍘' => '0点', + '㍙' => '1点', + '㍚' => '2点', + '㍛' => '3点', + '㍜' => '4点', + '㍝' => '5点', + '㍞' => '6点', + '㍟' => '7点', + '㍠' => '8点', + '㍡' => '9点', + '㍢' => '10点', + '㍣' => '11点', + '㍤' => '12点', + '㍥' => '13点', + '㍦' => '14点', + '㍧' => '15点', + '㍨' => '16点', + '㍩' => '17点', + '㍪' => '18点', + '㍫' => '19点', + '㍬' => '20点', + '㍭' => '21点', + '㍮' => '22点', + '㍯' => '23点', + '㍰' => '24点', + '㍱' => 'hPa', + '㍲' => 'da', + '㍳' => 'AU', + '㍴' => 'bar', + '㍵' => 'oV', + '㍶' => 'pc', + '㍷' => 'dm', + '㍸' => 'dm2', + '㍹' => 'dm3', + '㍺' => 'IU', + '㍻' => '平成', + '㍼' => '昭和', + '㍽' => '大正', + '㍾' => '明治', + '㍿' => '株式会社', + '㎀' => 'pA', + '㎁' => 'nA', + '㎂' => 'μA', + '㎃' => 'mA', + '㎄' => 'kA', + '㎅' => 'KB', + '㎆' => 'MB', + '㎇' => 'GB', + '㎈' => 'cal', + '㎉' => 'kcal', + '㎊' => 'pF', + '㎋' => 'nF', + '㎌' => 'μF', + '㎍' => 'μg', + '㎎' => 'mg', + '㎏' => 'kg', + '㎐' => 'Hz', + '㎑' => 'kHz', + '㎒' => 'MHz', + '㎓' => 'GHz', + '㎔' => 'THz', + '㎕' => 'μl', + '㎖' => 'ml', + '㎗' => 'dl', + '㎘' => 'kl', + '㎙' => 'fm', + '㎚' => 'nm', + '㎛' => 'μm', + '㎜' => 'mm', + '㎝' => 'cm', + '㎞' => 'km', + '㎟' => 'mm2', + '㎠' => 'cm2', + '㎡' => 'm2', + '㎢' => 'km2', + '㎣' => 'mm3', + '㎤' => 'cm3', + '㎥' => 'm3', + '㎦' => 'km3', + '㎧' => 'm∕s', + '㎨' => 'm∕s2', + '㎩' => 'Pa', + '㎪' => 'kPa', + '㎫' => 'MPa', + '㎬' => 'GPa', + '㎭' => 'rad', + '㎮' => 'rad∕s', + '㎯' => 'rad∕s2', + '㎰' => 'ps', + '㎱' => 'ns', + '㎲' => 'μs', + '㎳' => 'ms', + '㎴' => 'pV', + '㎵' => 'nV', + '㎶' => 'μV', + '㎷' => 'mV', + '㎸' => 'kV', + '㎹' => 'MV', + '㎺' => 'pW', + '㎻' => 'nW', + '㎼' => 'μW', + '㎽' => 'mW', + '㎾' => 'kW', + '㎿' => 'MW', + '㏀' => 'kΩ', + '㏁' => 'MΩ', + '㏂' => 'a.m.', + '㏃' => 'Bq', + '㏄' => 'cc', + '㏅' => 'cd', + '㏆' => 'C∕kg', + '㏇' => 'Co.', + '㏈' => 'dB', + '㏉' => 'Gy', + '㏊' => 'ha', + '㏋' => 'HP', + '㏌' => 'in', + '㏍' => 'KK', + '㏎' => 'KM', + '㏏' => 'kt', + '㏐' => 'lm', + '㏑' => 'ln', + '㏒' => 'log', + '㏓' => 'lx', + '㏔' => 'mb', + '㏕' => 'mil', + '㏖' => 'mol', + '㏗' => 'PH', + '㏘' => 'p.m.', + '㏙' => 'PPM', + '㏚' => 'PR', + '㏛' => 'sr', + '㏜' => 'Sv', + '㏝' => 'Wb', + '㏞' => 'V∕m', + '㏟' => 'A∕m', + '㏠' => '1日', + '㏡' => '2日', + '㏢' => '3日', + '㏣' => '4日', + '㏤' => '5日', + '㏥' => '6日', + '㏦' => '7日', + '㏧' => '8日', + '㏨' => '9日', + '㏩' => '10日', + '㏪' => '11日', + '㏫' => '12日', + '㏬' => '13日', + '㏭' => '14日', + '㏮' => '15日', + '㏯' => '16日', + '㏰' => '17日', + '㏱' => '18日', + '㏲' => '19日', + '㏳' => '20日', + '㏴' => '21日', + '㏵' => '22日', + '㏶' => '23日', + '㏷' => '24日', + '㏸' => '25日', + '㏹' => '26日', + '㏺' => '27日', + '㏻' => '28日', + '㏼' => '29日', + '㏽' => '30日', + '㏾' => '31日', + '㏿' => 'gal', + 'ꚜ' => 'ъ', + 'ꚝ' => 'ь', + 'ꝰ' => 'ꝯ', + 'ꟸ' => 'Ħ', + 'ꟹ' => 'œ', + 'ꭜ' => 'ꜧ', + 'ꭝ' => 'ꬷ', + 'ꭞ' => 'ɫ', + 'ꭟ' => 'ꭒ', + 'ꭩ' => 'ʍ', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + 'ſt' => 'st', + 'st' => 'st', + 'ﬓ' => 'մն', + 'ﬔ' => 'մե', + 'ﬕ' => 'մի', + 'ﬖ' => 'վն', + 'ﬗ' => 'մխ', + 'ﬠ' => 'ע', + 'ﬡ' => 'א', + 'ﬢ' => 'ד', + 'ﬣ' => 'ה', + 'ﬤ' => 'כ', + 'ﬥ' => 'ל', + 'ﬦ' => 'ם', + 'ﬧ' => 'ר', + 'ﬨ' => 'ת', + '﬩' => '+', + 'ﭏ' => 'אל', + 'ﭐ' => 'ٱ', + 'ﭑ' => 'ٱ', + 'ﭒ' => 'ٻ', + 'ﭓ' => 'ٻ', + 'ﭔ' => 'ٻ', + 'ﭕ' => 'ٻ', + 'ﭖ' => 'پ', + 'ﭗ' => 'پ', + 'ﭘ' => 'پ', + 'ﭙ' => 'پ', + 'ﭚ' => 'ڀ', + 'ﭛ' => 'ڀ', + 'ﭜ' => 'ڀ', + 'ﭝ' => 'ڀ', + 'ﭞ' => 'ٺ', + 'ﭟ' => 'ٺ', + 'ﭠ' => 'ٺ', + 'ﭡ' => 'ٺ', + 'ﭢ' => 'ٿ', + 'ﭣ' => 'ٿ', + 'ﭤ' => 'ٿ', + 'ﭥ' => 'ٿ', + 'ﭦ' => 'ٹ', + 'ﭧ' => 'ٹ', + 'ﭨ' => 'ٹ', + 'ﭩ' => 'ٹ', + 'ﭪ' => 'ڤ', + 'ﭫ' => 'ڤ', + 'ﭬ' => 'ڤ', + 'ﭭ' => 'ڤ', + 'ﭮ' => 'ڦ', + 'ﭯ' => 'ڦ', + 'ﭰ' => 'ڦ', + 'ﭱ' => 'ڦ', + 'ﭲ' => 'ڄ', + 'ﭳ' => 'ڄ', + 'ﭴ' => 'ڄ', + 'ﭵ' => 'ڄ', + 'ﭶ' => 'ڃ', + 'ﭷ' => 'ڃ', + 'ﭸ' => 'ڃ', + 'ﭹ' => 'ڃ', + 'ﭺ' => 'چ', + 'ﭻ' => 'چ', + 'ﭼ' => 'چ', + 'ﭽ' => 'چ', + 'ﭾ' => 'ڇ', + 'ﭿ' => 'ڇ', + 'ﮀ' => 'ڇ', + 'ﮁ' => 'ڇ', + 'ﮂ' => 'ڍ', + 'ﮃ' => 'ڍ', + 'ﮄ' => 'ڌ', + 'ﮅ' => 'ڌ', + 'ﮆ' => 'ڎ', + 'ﮇ' => 'ڎ', + 'ﮈ' => 'ڈ', + 'ﮉ' => 'ڈ', + 'ﮊ' => 'ژ', + 'ﮋ' => 'ژ', + 'ﮌ' => 'ڑ', + 'ﮍ' => 'ڑ', + 'ﮎ' => 'ک', + 'ﮏ' => 'ک', + 'ﮐ' => 'ک', + 'ﮑ' => 'ک', + 'ﮒ' => 'گ', + 'ﮓ' => 'گ', + 'ﮔ' => 'گ', + 'ﮕ' => 'گ', + 'ﮖ' => 'ڳ', + 'ﮗ' => 'ڳ', + 'ﮘ' => 'ڳ', + 'ﮙ' => 'ڳ', + 'ﮚ' => 'ڱ', + 'ﮛ' => 'ڱ', + 'ﮜ' => 'ڱ', + 'ﮝ' => 'ڱ', + 'ﮞ' => 'ں', + 'ﮟ' => 'ں', + 'ﮠ' => 'ڻ', + 'ﮡ' => 'ڻ', + 'ﮢ' => 'ڻ', + 'ﮣ' => 'ڻ', + 'ﮤ' => 'ۀ', + 'ﮥ' => 'ۀ', + 'ﮦ' => 'ہ', + 'ﮧ' => 'ہ', + 'ﮨ' => 'ہ', + 'ﮩ' => 'ہ', + 'ﮪ' => 'ھ', + 'ﮫ' => 'ھ', + 'ﮬ' => 'ھ', + 'ﮭ' => 'ھ', + 'ﮮ' => 'ے', + 'ﮯ' => 'ے', + 'ﮰ' => 'ۓ', + 'ﮱ' => 'ۓ', + 'ﯓ' => 'ڭ', + 'ﯔ' => 'ڭ', + 'ﯕ' => 'ڭ', + 'ﯖ' => 'ڭ', + 'ﯗ' => 'ۇ', + 'ﯘ' => 'ۇ', + 'ﯙ' => 'ۆ', + 'ﯚ' => 'ۆ', + 'ﯛ' => 'ۈ', + 'ﯜ' => 'ۈ', + 'ﯝ' => 'ۇٴ', + 'ﯞ' => 'ۋ', + 'ﯟ' => 'ۋ', + 'ﯠ' => 'ۅ', + 'ﯡ' => 'ۅ', + 'ﯢ' => 'ۉ', + 'ﯣ' => 'ۉ', + 'ﯤ' => 'ې', + 'ﯥ' => 'ې', + 'ﯦ' => 'ې', + 'ﯧ' => 'ې', + 'ﯨ' => 'ى', + 'ﯩ' => 'ى', + 'ﯪ' => 'ئا', + 'ﯫ' => 'ئا', + 'ﯬ' => 'ئە', + 'ﯭ' => 'ئە', + 'ﯮ' => 'ئو', + 'ﯯ' => 'ئو', + 'ﯰ' => 'ئۇ', + 'ﯱ' => 'ئۇ', + 'ﯲ' => 'ئۆ', + 'ﯳ' => 'ئۆ', + 'ﯴ' => 'ئۈ', + 'ﯵ' => 'ئۈ', + 'ﯶ' => 'ئې', + 'ﯷ' => 'ئې', + 'ﯸ' => 'ئې', + 'ﯹ' => 'ئى', + 'ﯺ' => 'ئى', + 'ﯻ' => 'ئى', + 'ﯼ' => 'ی', + 'ﯽ' => 'ی', + 'ﯾ' => 'ی', + 'ﯿ' => 'ی', + 'ﰀ' => 'ئج', + 'ﰁ' => 'ئح', + 'ﰂ' => 'ئم', + 'ﰃ' => 'ئى', + 'ﰄ' => 'ئي', + 'ﰅ' => 'بج', + 'ﰆ' => 'بح', + 'ﰇ' => 'بخ', + 'ﰈ' => 'بم', + 'ﰉ' => 'بى', + 'ﰊ' => 'بي', + 'ﰋ' => 'تج', + 'ﰌ' => 'تح', + 'ﰍ' => 'تخ', + 'ﰎ' => 'تم', + 'ﰏ' => 'تى', + 'ﰐ' => 'تي', + 'ﰑ' => 'ثج', + 'ﰒ' => 'ثم', + 'ﰓ' => 'ثى', + 'ﰔ' => 'ثي', + 'ﰕ' => 'جح', + 'ﰖ' => 'جم', + 'ﰗ' => 'حج', + 'ﰘ' => 'حم', + 'ﰙ' => 'خج', + 'ﰚ' => 'خح', + 'ﰛ' => 'خم', + 'ﰜ' => 'سج', + 'ﰝ' => 'سح', + 'ﰞ' => 'سخ', + 'ﰟ' => 'سم', + 'ﰠ' => 'صح', + 'ﰡ' => 'صم', + 'ﰢ' => 'ضج', + 'ﰣ' => 'ضح', + 'ﰤ' => 'ضخ', + 'ﰥ' => 'ضم', + 'ﰦ' => 'طح', + 'ﰧ' => 'طم', + 'ﰨ' => 'ظم', + 'ﰩ' => 'عج', + 'ﰪ' => 'عم', + 'ﰫ' => 'غج', + 'ﰬ' => 'غم', + 'ﰭ' => 'فج', + 'ﰮ' => 'فح', + 'ﰯ' => 'فخ', + 'ﰰ' => 'فم', + 'ﰱ' => 'فى', + 'ﰲ' => 'في', + 'ﰳ' => 'قح', + 'ﰴ' => 'قم', + 'ﰵ' => 'قى', + 'ﰶ' => 'قي', + 'ﰷ' => 'كا', + 'ﰸ' => 'كج', + 'ﰹ' => 'كح', + 'ﰺ' => 'كخ', + 'ﰻ' => 'كل', + 'ﰼ' => 'كم', + 'ﰽ' => 'كى', + 'ﰾ' => 'كي', + 'ﰿ' => 'لج', + 'ﱀ' => 'لح', + 'ﱁ' => 'لخ', + 'ﱂ' => 'لم', + 'ﱃ' => 'لى', + 'ﱄ' => 'لي', + 'ﱅ' => 'مج', + 'ﱆ' => 'مح', + 'ﱇ' => 'مخ', + 'ﱈ' => 'مم', + 'ﱉ' => 'مى', + 'ﱊ' => 'مي', + 'ﱋ' => 'نج', + 'ﱌ' => 'نح', + 'ﱍ' => 'نخ', + 'ﱎ' => 'نم', + 'ﱏ' => 'نى', + 'ﱐ' => 'ني', + 'ﱑ' => 'هج', + 'ﱒ' => 'هم', + 'ﱓ' => 'هى', + 'ﱔ' => 'هي', + 'ﱕ' => 'يج', + 'ﱖ' => 'يح', + 'ﱗ' => 'يخ', + 'ﱘ' => 'يم', + 'ﱙ' => 'يى', + 'ﱚ' => 'يي', + 'ﱛ' => 'ذٰ', + 'ﱜ' => 'رٰ', + 'ﱝ' => 'ىٰ', + 'ﱞ' => ' ٌّ', + 'ﱟ' => ' ٍّ', + 'ﱠ' => ' َّ', + 'ﱡ' => ' ُّ', + 'ﱢ' => ' ِّ', + 'ﱣ' => ' ّٰ', + 'ﱤ' => 'ئر', + 'ﱥ' => 'ئز', + 'ﱦ' => 'ئم', + 'ﱧ' => 'ئن', + 'ﱨ' => 'ئى', + 'ﱩ' => 'ئي', + 'ﱪ' => 'بر', + 'ﱫ' => 'بز', + 'ﱬ' => 'بم', + 'ﱭ' => 'بن', + 'ﱮ' => 'بى', + 'ﱯ' => 'بي', + 'ﱰ' => 'تر', + 'ﱱ' => 'تز', + 'ﱲ' => 'تم', + 'ﱳ' => 'تن', + 'ﱴ' => 'تى', + 'ﱵ' => 'تي', + 'ﱶ' => 'ثر', + 'ﱷ' => 'ثز', + 'ﱸ' => 'ثم', + 'ﱹ' => 'ثن', + 'ﱺ' => 'ثى', + 'ﱻ' => 'ثي', + 'ﱼ' => 'فى', + 'ﱽ' => 'في', + 'ﱾ' => 'قى', + 'ﱿ' => 'قي', + 'ﲀ' => 'كا', + 'ﲁ' => 'كل', + 'ﲂ' => 'كم', + 'ﲃ' => 'كى', + 'ﲄ' => 'كي', + 'ﲅ' => 'لم', + 'ﲆ' => 'لى', + 'ﲇ' => 'لي', + 'ﲈ' => 'ما', + 'ﲉ' => 'مم', + 'ﲊ' => 'نر', + 'ﲋ' => 'نز', + 'ﲌ' => 'نم', + 'ﲍ' => 'نن', + 'ﲎ' => 'نى', + 'ﲏ' => 'ني', + 'ﲐ' => 'ىٰ', + 'ﲑ' => 'ير', + 'ﲒ' => 'يز', + 'ﲓ' => 'يم', + 'ﲔ' => 'ين', + 'ﲕ' => 'يى', + 'ﲖ' => 'يي', + 'ﲗ' => 'ئج', + 'ﲘ' => 'ئح', + 'ﲙ' => 'ئخ', + 'ﲚ' => 'ئم', + 'ﲛ' => 'ئه', + 'ﲜ' => 'بج', + 'ﲝ' => 'بح', + 'ﲞ' => 'بخ', + 'ﲟ' => 'بم', + 'ﲠ' => 'به', + 'ﲡ' => 'تج', + 'ﲢ' => 'تح', + 'ﲣ' => 'تخ', + 'ﲤ' => 'تم', + 'ﲥ' => 'ته', + 'ﲦ' => 'ثم', + 'ﲧ' => 'جح', + 'ﲨ' => 'جم', + 'ﲩ' => 'حج', + 'ﲪ' => 'حم', + 'ﲫ' => 'خج', + 'ﲬ' => 'خم', + 'ﲭ' => 'سج', + 'ﲮ' => 'سح', + 'ﲯ' => 'سخ', + 'ﲰ' => 'سم', + 'ﲱ' => 'صح', + 'ﲲ' => 'صخ', + 'ﲳ' => 'صم', + 'ﲴ' => 'ضج', + 'ﲵ' => 'ضح', + 'ﲶ' => 'ضخ', + 'ﲷ' => 'ضم', + 'ﲸ' => 'طح', + 'ﲹ' => 'ظم', + 'ﲺ' => 'عج', + 'ﲻ' => 'عم', + 'ﲼ' => 'غج', + 'ﲽ' => 'غم', + 'ﲾ' => 'فج', + 'ﲿ' => 'فح', + 'ﳀ' => 'فخ', + 'ﳁ' => 'فم', + 'ﳂ' => 'قح', + 'ﳃ' => 'قم', + 'ﳄ' => 'كج', + 'ﳅ' => 'كح', + 'ﳆ' => 'كخ', + 'ﳇ' => 'كل', + 'ﳈ' => 'كم', + 'ﳉ' => 'لج', + 'ﳊ' => 'لح', + 'ﳋ' => 'لخ', + 'ﳌ' => 'لم', + 'ﳍ' => 'له', + 'ﳎ' => 'مج', + 'ﳏ' => 'مح', + 'ﳐ' => 'مخ', + 'ﳑ' => 'مم', + 'ﳒ' => 'نج', + 'ﳓ' => 'نح', + 'ﳔ' => 'نخ', + 'ﳕ' => 'نم', + 'ﳖ' => 'نه', + 'ﳗ' => 'هج', + 'ﳘ' => 'هم', + 'ﳙ' => 'هٰ', + 'ﳚ' => 'يج', + 'ﳛ' => 'يح', + 'ﳜ' => 'يخ', + 'ﳝ' => 'يم', + 'ﳞ' => 'يه', + 'ﳟ' => 'ئم', + 'ﳠ' => 'ئه', + 'ﳡ' => 'بم', + 'ﳢ' => 'به', + 'ﳣ' => 'تم', + 'ﳤ' => 'ته', + 'ﳥ' => 'ثم', + 'ﳦ' => 'ثه', + 'ﳧ' => 'سم', + 'ﳨ' => 'سه', + 'ﳩ' => 'شم', + 'ﳪ' => 'شه', + 'ﳫ' => 'كل', + 'ﳬ' => 'كم', + 'ﳭ' => 'لم', + 'ﳮ' => 'نم', + 'ﳯ' => 'نه', + 'ﳰ' => 'يم', + 'ﳱ' => 'يه', + 'ﳲ' => 'ـَّ', + 'ﳳ' => 'ـُّ', + 'ﳴ' => 'ـِّ', + 'ﳵ' => 'طى', + 'ﳶ' => 'طي', + 'ﳷ' => 'عى', + 'ﳸ' => 'عي', + 'ﳹ' => 'غى', + 'ﳺ' => 'غي', + 'ﳻ' => 'سى', + 'ﳼ' => 'سي', + 'ﳽ' => 'شى', + 'ﳾ' => 'شي', + 'ﳿ' => 'حى', + 'ﴀ' => 'حي', + 'ﴁ' => 'جى', + 'ﴂ' => 'جي', + 'ﴃ' => 'خى', + 'ﴄ' => 'خي', + 'ﴅ' => 'صى', + 'ﴆ' => 'صي', + 'ﴇ' => 'ضى', + 'ﴈ' => 'ضي', + 'ﴉ' => 'شج', + 'ﴊ' => 'شح', + 'ﴋ' => 'شخ', + 'ﴌ' => 'شم', + 'ﴍ' => 'شر', + 'ﴎ' => 'سر', + 'ﴏ' => 'صر', + 'ﴐ' => 'ضر', + 'ﴑ' => 'طى', + 'ﴒ' => 'طي', + 'ﴓ' => 'عى', + 'ﴔ' => 'عي', + 'ﴕ' => 'غى', + 'ﴖ' => 'غي', + 'ﴗ' => 'سى', + 'ﴘ' => 'سي', + 'ﴙ' => 'شى', + 'ﴚ' => 'شي', + 'ﴛ' => 'حى', + 'ﴜ' => 'حي', + 'ﴝ' => 'جى', + 'ﴞ' => 'جي', + 'ﴟ' => 'خى', + 'ﴠ' => 'خي', + 'ﴡ' => 'صى', + 'ﴢ' => 'صي', + 'ﴣ' => 'ضى', + 'ﴤ' => 'ضي', + 'ﴥ' => 'شج', + 'ﴦ' => 'شح', + 'ﴧ' => 'شخ', + 'ﴨ' => 'شم', + 'ﴩ' => 'شر', + 'ﴪ' => 'سر', + 'ﴫ' => 'صر', + 'ﴬ' => 'ضر', + 'ﴭ' => 'شج', + 'ﴮ' => 'شح', + 'ﴯ' => 'شخ', + 'ﴰ' => 'شم', + 'ﴱ' => 'سه', + 'ﴲ' => 'شه', + 'ﴳ' => 'طم', + 'ﴴ' => 'سج', + 'ﴵ' => 'سح', + 'ﴶ' => 'سخ', + 'ﴷ' => 'شج', + 'ﴸ' => 'شح', + 'ﴹ' => 'شخ', + 'ﴺ' => 'طم', + 'ﴻ' => 'ظم', + 'ﴼ' => 'اً', + 'ﴽ' => 'اً', + 'ﵐ' => 'تجم', + 'ﵑ' => 'تحج', + 'ﵒ' => 'تحج', + 'ﵓ' => 'تحم', + 'ﵔ' => 'تخم', + 'ﵕ' => 'تمج', + 'ﵖ' => 'تمح', + 'ﵗ' => 'تمخ', + 'ﵘ' => 'جمح', + 'ﵙ' => 'جمح', + 'ﵚ' => 'حمي', + 'ﵛ' => 'حمى', + 'ﵜ' => 'سحج', + 'ﵝ' => 'سجح', + 'ﵞ' => 'سجى', + 'ﵟ' => 'سمح', + 'ﵠ' => 'سمح', + 'ﵡ' => 'سمج', + 'ﵢ' => 'سمم', + 'ﵣ' => 'سمم', + 'ﵤ' => 'صحح', + 'ﵥ' => 'صحح', + 'ﵦ' => 'صمم', + 'ﵧ' => 'شحم', + 'ﵨ' => 'شحم', + 'ﵩ' => 'شجي', + 'ﵪ' => 'شمخ', + 'ﵫ' => 'شمخ', + 'ﵬ' => 'شمم', + 'ﵭ' => 'شمم', + 'ﵮ' => 'ضحى', + 'ﵯ' => 'ضخم', + 'ﵰ' => 'ضخم', + 'ﵱ' => 'طمح', + 'ﵲ' => 'طمح', + 'ﵳ' => 'طمم', + 'ﵴ' => 'طمي', + 'ﵵ' => 'عجم', + 'ﵶ' => 'عمم', + 'ﵷ' => 'عمم', + 'ﵸ' => 'عمى', + 'ﵹ' => 'غمم', + 'ﵺ' => 'غمي', + 'ﵻ' => 'غمى', + 'ﵼ' => 'فخم', + 'ﵽ' => 'فخم', + 'ﵾ' => 'قمح', + 'ﵿ' => 'قمم', + 'ﶀ' => 'لحم', + 'ﶁ' => 'لحي', + 'ﶂ' => 'لحى', + 'ﶃ' => 'لجج', + 'ﶄ' => 'لجج', + 'ﶅ' => 'لخم', + 'ﶆ' => 'لخم', + 'ﶇ' => 'لمح', + 'ﶈ' => 'لمح', + 'ﶉ' => 'محج', + 'ﶊ' => 'محم', + 'ﶋ' => 'محي', + 'ﶌ' => 'مجح', + 'ﶍ' => 'مجم', + 'ﶎ' => 'مخج', + 'ﶏ' => 'مخم', + 'ﶒ' => 'مجخ', + 'ﶓ' => 'همج', + 'ﶔ' => 'همم', + 'ﶕ' => 'نحم', + 'ﶖ' => 'نحى', + 'ﶗ' => 'نجم', + 'ﶘ' => 'نجم', + 'ﶙ' => 'نجى', + 'ﶚ' => 'نمي', + 'ﶛ' => 'نمى', + 'ﶜ' => 'يمم', + 'ﶝ' => 'يمم', + 'ﶞ' => 'بخي', + 'ﶟ' => 'تجي', + 'ﶠ' => 'تجى', + 'ﶡ' => 'تخي', + 'ﶢ' => 'تخى', + 'ﶣ' => 'تمي', + 'ﶤ' => 'تمى', + 'ﶥ' => 'جمي', + 'ﶦ' => 'جحى', + 'ﶧ' => 'جمى', + 'ﶨ' => 'سخى', + 'ﶩ' => 'صحي', + 'ﶪ' => 'شحي', + 'ﶫ' => 'ضحي', + 'ﶬ' => 'لجي', + 'ﶭ' => 'لمي', + 'ﶮ' => 'يحي', + 'ﶯ' => 'يجي', + 'ﶰ' => 'يمي', + 'ﶱ' => 'ممي', + 'ﶲ' => 'قمي', + 'ﶳ' => 'نحي', + 'ﶴ' => 'قمح', + 'ﶵ' => 'لحم', + 'ﶶ' => 'عمي', + 'ﶷ' => 'كمي', + 'ﶸ' => 'نجح', + 'ﶹ' => 'مخي', + 'ﶺ' => 'لجم', + 'ﶻ' => 'كمم', + 'ﶼ' => 'لجم', + 'ﶽ' => 'نجح', + 'ﶾ' => 'جحي', + 'ﶿ' => 'حجي', + 'ﷀ' => 'مجي', + 'ﷁ' => 'فمي', + 'ﷂ' => 'بحي', + 'ﷃ' => 'كمم', + 'ﷄ' => 'عجم', + 'ﷅ' => 'صمم', + 'ﷆ' => 'سخي', + 'ﷇ' => 'نجي', + 'ﷰ' => 'صلے', + 'ﷱ' => 'قلے', + 'ﷲ' => 'الله', + 'ﷳ' => 'اكبر', + 'ﷴ' => 'محمد', + 'ﷵ' => 'صلعم', + 'ﷶ' => 'رسول', + 'ﷷ' => 'عليه', + 'ﷸ' => 'وسلم', + 'ﷹ' => 'صلى', + 'ﷺ' => 'صلى الله عليه وسلم', + 'ﷻ' => 'جل جلاله', + '﷼' => 'ریال', + '︐' => ',', + '︑' => '、', + '︒' => '。', + '︓' => ':', + '︔' => ';', + '︕' => '!', + '︖' => '?', + '︗' => '〖', + '︘' => '〗', + '︙' => '...', + '︰' => '..', + '︱' => '—', + '︲' => '–', + '︳' => '_', + '︴' => '_', + '︵' => '(', + '︶' => ')', + '︷' => '{', + '︸' => '}', + '︹' => '〔', + '︺' => '〕', + '︻' => '【', + '︼' => '】', + '︽' => '《', + '︾' => '》', + '︿' => '〈', + '﹀' => '〉', + '﹁' => '「', + '﹂' => '」', + '﹃' => '『', + '﹄' => '』', + '﹇' => '[', + '﹈' => ']', + '﹉' => ' ̅', + '﹊' => ' ̅', + '﹋' => ' ̅', + '﹌' => ' ̅', + '﹍' => '_', + '﹎' => '_', + '﹏' => '_', + '﹐' => ',', + '﹑' => '、', + '﹒' => '.', + '﹔' => ';', + '﹕' => ':', + '﹖' => '?', + '﹗' => '!', + '﹘' => '—', + '﹙' => '(', + '﹚' => ')', + '﹛' => '{', + '﹜' => '}', + '﹝' => '〔', + '﹞' => '〕', + '﹟' => '#', + '﹠' => '&', + '﹡' => '*', + '﹢' => '+', + '﹣' => '-', + '﹤' => '<', + '﹥' => '>', + '﹦' => '=', + '﹨' => '\\', + '﹩' => '$', + '﹪' => '%', + '﹫' => '@', + 'ﹰ' => ' ً', + 'ﹱ' => 'ـً', + 'ﹲ' => ' ٌ', + 'ﹴ' => ' ٍ', + 'ﹶ' => ' َ', + 'ﹷ' => 'ـَ', + 'ﹸ' => ' ُ', + 'ﹹ' => 'ـُ', + 'ﹺ' => ' ِ', + 'ﹻ' => 'ـِ', + 'ﹼ' => ' ّ', + 'ﹽ' => 'ـّ', + 'ﹾ' => ' ْ', + 'ﹿ' => 'ـْ', + 'ﺀ' => 'ء', + 'ﺁ' => 'آ', + 'ﺂ' => 'آ', + 'ﺃ' => 'أ', + 'ﺄ' => 'أ', + 'ﺅ' => 'ؤ', + 'ﺆ' => 'ؤ', + 'ﺇ' => 'إ', + 'ﺈ' => 'إ', + 'ﺉ' => 'ئ', + 'ﺊ' => 'ئ', + 'ﺋ' => 'ئ', + 'ﺌ' => 'ئ', + 'ﺍ' => 'ا', + 'ﺎ' => 'ا', + 'ﺏ' => 'ب', + 'ﺐ' => 'ب', + 'ﺑ' => 'ب', + 'ﺒ' => 'ب', + 'ﺓ' => 'ة', + 'ﺔ' => 'ة', + 'ﺕ' => 'ت', + 'ﺖ' => 'ت', + 'ﺗ' => 'ت', + 'ﺘ' => 'ت', + 'ﺙ' => 'ث', + 'ﺚ' => 'ث', + 'ﺛ' => 'ث', + 'ﺜ' => 'ث', + 'ﺝ' => 'ج', + 'ﺞ' => 'ج', + 'ﺟ' => 'ج', + 'ﺠ' => 'ج', + 'ﺡ' => 'ح', + 'ﺢ' => 'ح', + 'ﺣ' => 'ح', + 'ﺤ' => 'ح', + 'ﺥ' => 'خ', + 'ﺦ' => 'خ', + 'ﺧ' => 'خ', + 'ﺨ' => 'خ', + 'ﺩ' => 'د', + 'ﺪ' => 'د', + 'ﺫ' => 'ذ', + 'ﺬ' => 'ذ', + 'ﺭ' => 'ر', + 'ﺮ' => 'ر', + 'ﺯ' => 'ز', + 'ﺰ' => 'ز', + 'ﺱ' => 'س', + 'ﺲ' => 'س', + 'ﺳ' => 'س', + 'ﺴ' => 'س', + 'ﺵ' => 'ش', + 'ﺶ' => 'ش', + 'ﺷ' => 'ش', + 'ﺸ' => 'ش', + 'ﺹ' => 'ص', + 'ﺺ' => 'ص', + 'ﺻ' => 'ص', + 'ﺼ' => 'ص', + 'ﺽ' => 'ض', + 'ﺾ' => 'ض', + 'ﺿ' => 'ض', + 'ﻀ' => 'ض', + 'ﻁ' => 'ط', + 'ﻂ' => 'ط', + 'ﻃ' => 'ط', + 'ﻄ' => 'ط', + 'ﻅ' => 'ظ', + 'ﻆ' => 'ظ', + 'ﻇ' => 'ظ', + 'ﻈ' => 'ظ', + 'ﻉ' => 'ع', + 'ﻊ' => 'ع', + 'ﻋ' => 'ع', + 'ﻌ' => 'ع', + 'ﻍ' => 'غ', + 'ﻎ' => 'غ', + 'ﻏ' => 'غ', + 'ﻐ' => 'غ', + 'ﻑ' => 'ف', + 'ﻒ' => 'ف', + 'ﻓ' => 'ف', + 'ﻔ' => 'ف', + 'ﻕ' => 'ق', + 'ﻖ' => 'ق', + 'ﻗ' => 'ق', + 'ﻘ' => 'ق', + 'ﻙ' => 'ك', + 'ﻚ' => 'ك', + 'ﻛ' => 'ك', + 'ﻜ' => 'ك', + 'ﻝ' => 'ل', + 'ﻞ' => 'ل', + 'ﻟ' => 'ل', + 'ﻠ' => 'ل', + 'ﻡ' => 'م', + 'ﻢ' => 'م', + 'ﻣ' => 'م', + 'ﻤ' => 'م', + 'ﻥ' => 'ن', + 'ﻦ' => 'ن', + 'ﻧ' => 'ن', + 'ﻨ' => 'ن', + 'ﻩ' => 'ه', + 'ﻪ' => 'ه', + 'ﻫ' => 'ه', + 'ﻬ' => 'ه', + 'ﻭ' => 'و', + 'ﻮ' => 'و', + 'ﻯ' => 'ى', + 'ﻰ' => 'ى', + 'ﻱ' => 'ي', + 'ﻲ' => 'ي', + 'ﻳ' => 'ي', + 'ﻴ' => 'ي', + 'ﻵ' => 'لآ', + 'ﻶ' => 'لآ', + 'ﻷ' => 'لأ', + 'ﻸ' => 'لأ', + 'ﻹ' => 'لإ', + 'ﻺ' => 'لإ', + 'ﻻ' => 'لا', + 'ﻼ' => 'لا', + '!' => '!', + '"' => '"', + '#' => '#', + '$' => '$', + '%' => '%', + '&' => '&', + ''' => '\'', + '(' => '(', + ')' => ')', + '*' => '*', + '+' => '+', + ',' => ',', + '-' => '-', + '.' => '.', + '/' => '/', + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + '5' => '5', + '6' => '6', + '7' => '7', + '8' => '8', + '9' => '9', + ':' => ':', + ';' => ';', + '<' => '<', + '=' => '=', + '>' => '>', + '?' => '?', + '@' => '@', + 'A' => 'A', + 'B' => 'B', + 'C' => 'C', + 'D' => 'D', + 'E' => 'E', + 'F' => 'F', + 'G' => 'G', + 'H' => 'H', + 'I' => 'I', + 'J' => 'J', + 'K' => 'K', + 'L' => 'L', + 'M' => 'M', + 'N' => 'N', + 'O' => 'O', + 'P' => 'P', + 'Q' => 'Q', + 'R' => 'R', + 'S' => 'S', + 'T' => 'T', + 'U' => 'U', + 'V' => 'V', + 'W' => 'W', + 'X' => 'X', + 'Y' => 'Y', + 'Z' => 'Z', + '[' => '[', + '\' => '\\', + ']' => ']', + '^' => '^', + '_' => '_', + '`' => '`', + 'a' => 'a', + 'b' => 'b', + 'c' => 'c', + 'd' => 'd', + 'e' => 'e', + 'f' => 'f', + 'g' => 'g', + 'h' => 'h', + 'i' => 'i', + 'j' => 'j', + 'k' => 'k', + 'l' => 'l', + 'm' => 'm', + 'n' => 'n', + 'o' => 'o', + 'p' => 'p', + 'q' => 'q', + 'r' => 'r', + 's' => 's', + 't' => 't', + 'u' => 'u', + 'v' => 'v', + 'w' => 'w', + 'x' => 'x', + 'y' => 'y', + 'z' => 'z', + '{' => '{', + '|' => '|', + '}' => '}', + '~' => '~', + '⦅' => '⦅', + '⦆' => '⦆', + '。' => '。', + '「' => '「', + '」' => '」', + '、' => '、', + '・' => '・', + 'ヲ' => 'ヲ', + 'ァ' => 'ァ', + 'ィ' => 'ィ', + 'ゥ' => 'ゥ', + 'ェ' => 'ェ', + 'ォ' => 'ォ', + 'ャ' => 'ャ', + 'ュ' => 'ュ', + 'ョ' => 'ョ', + 'ッ' => 'ッ', + 'ー' => 'ー', + 'ア' => 'ア', + 'イ' => 'イ', + 'ウ' => 'ウ', + 'エ' => 'エ', + 'オ' => 'オ', + 'カ' => 'カ', + 'キ' => 'キ', + 'ク' => 'ク', + 'ケ' => 'ケ', + 'コ' => 'コ', + 'サ' => 'サ', + 'シ' => 'シ', + 'ス' => 'ス', + 'セ' => 'セ', + 'ソ' => 'ソ', + 'タ' => 'タ', + 'チ' => 'チ', + 'ツ' => 'ツ', + 'テ' => 'テ', + 'ト' => 'ト', + 'ナ' => 'ナ', + 'ニ' => 'ニ', + 'ヌ' => 'ヌ', + 'ネ' => 'ネ', + 'ノ' => 'ノ', + 'ハ' => 'ハ', + 'ヒ' => 'ヒ', + 'フ' => 'フ', + 'ヘ' => 'ヘ', + 'ホ' => 'ホ', + 'マ' => 'マ', + 'ミ' => 'ミ', + 'ム' => 'ム', + 'メ' => 'メ', + 'モ' => 'モ', + 'ヤ' => 'ヤ', + 'ユ' => 'ユ', + 'ヨ' => 'ヨ', + 'ラ' => 'ラ', + 'リ' => 'リ', + 'ル' => 'ル', + 'レ' => 'レ', + 'ロ' => 'ロ', + 'ワ' => 'ワ', + 'ン' => 'ン', + '゙' => '゙', + '゚' => '゚', + 'ᅠ' => 'ᅠ', + 'ᄀ' => 'ᄀ', + 'ᄁ' => 'ᄁ', + 'ᆪ' => 'ᆪ', + 'ᄂ' => 'ᄂ', + 'ᆬ' => 'ᆬ', + 'ᆭ' => 'ᆭ', + 'ᄃ' => 'ᄃ', + 'ᄄ' => 'ᄄ', + 'ᄅ' => 'ᄅ', + 'ᆰ' => 'ᆰ', + 'ᆱ' => 'ᆱ', + 'ᆲ' => 'ᆲ', + 'ᆳ' => 'ᆳ', + 'ᆴ' => 'ᆴ', + 'ᆵ' => 'ᆵ', + 'ᄚ' => 'ᄚ', + 'ᄆ' => 'ᄆ', + 'ᄇ' => 'ᄇ', + 'ᄈ' => 'ᄈ', + 'ᄡ' => 'ᄡ', + 'ᄉ' => 'ᄉ', + 'ᄊ' => 'ᄊ', + 'ᄋ' => 'ᄋ', + 'ᄌ' => 'ᄌ', + 'ᄍ' => 'ᄍ', + 'ᄎ' => 'ᄎ', + 'ᄏ' => 'ᄏ', + 'ᄐ' => 'ᄐ', + 'ᄑ' => 'ᄑ', + 'ᄒ' => 'ᄒ', + 'ᅡ' => 'ᅡ', + 'ᅢ' => 'ᅢ', + 'ᅣ' => 'ᅣ', + 'ᅤ' => 'ᅤ', + 'ᅥ' => 'ᅥ', + 'ᅦ' => 'ᅦ', + 'ᅧ' => 'ᅧ', + 'ᅨ' => 'ᅨ', + 'ᅩ' => 'ᅩ', + 'ᅪ' => 'ᅪ', + 'ᅫ' => 'ᅫ', + 'ᅬ' => 'ᅬ', + 'ᅭ' => 'ᅭ', + 'ᅮ' => 'ᅮ', + 'ᅯ' => 'ᅯ', + 'ᅰ' => 'ᅰ', + 'ᅱ' => 'ᅱ', + 'ᅲ' => 'ᅲ', + 'ᅳ' => 'ᅳ', + 'ᅴ' => 'ᅴ', + 'ᅵ' => 'ᅵ', + '¢' => '¢', + '£' => '£', + '¬' => '¬', + ' ̄' => ' ̄', + '¦' => '¦', + '¥' => '¥', + '₩' => '₩', + '│' => '│', + '←' => '←', + '↑' => '↑', + '→' => '→', + '↓' => '↓', + '■' => '■', + '○' => '○', + '𝐀' => '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', + '𝐚' => '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', + '𝐴' => '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', + '𝑎' => 'a', + '𝑏' => 'b', + '𝑐' => 'c', + '𝑑' => 'd', + '𝑒' => 'e', + '𝑓' => 'f', + '𝑔' => 'g', + '𝑖' => 'i', + '𝑗' => 'j', + '𝑘' => 'k', + '𝑙' => 'l', + '𝑚' => 'm', + '𝑛' => 'n', + '𝑜' => 'o', + '𝑝' => 'p', + '𝑞' => 'q', + '𝑟' => 'r', + '𝑠' => 's', + '𝑡' => 't', + '𝑢' => 'u', + '𝑣' => 'v', + '𝑤' => 'w', + '𝑥' => 'x', + '𝑦' => 'y', + '𝑧' => 'z', + '𝑨' => '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', + '𝒂' => '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', + '𝒜' => 'A', + '𝒞' => 'C', + '𝒟' => 'D', + '𝒢' => 'G', + '𝒥' => 'J', + '𝒦' => 'K', + '𝒩' => 'N', + '𝒪' => 'O', + '𝒫' => 'P', + '𝒬' => 'Q', + '𝒮' => 'S', + '𝒯' => 'T', + '𝒰' => 'U', + '𝒱' => 'V', + '𝒲' => 'W', + '𝒳' => 'X', + '𝒴' => 'Y', + '𝒵' => 'Z', + '𝒶' => 'a', + '𝒷' => 'b', + '𝒸' => 'c', + '𝒹' => 'd', + '𝒻' => 'f', + '𝒽' => 'h', + '𝒾' => 'i', + '𝒿' => 'j', + '𝓀' => 'k', + '𝓁' => 'l', + '𝓂' => 'm', + '𝓃' => 'n', + '𝓅' => 'p', + '𝓆' => 'q', + '𝓇' => 'r', + '𝓈' => 's', + '𝓉' => 't', + '𝓊' => 'u', + '𝓋' => 'v', + '𝓌' => 'w', + '𝓍' => 'x', + '𝓎' => 'y', + '𝓏' => 'z', + '𝓐' => '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', + '𝓪' => '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', + '𝔄' => 'A', + '𝔅' => 'B', + '𝔇' => 'D', + '𝔈' => 'E', + '𝔉' => 'F', + '𝔊' => 'G', + '𝔍' => 'J', + '𝔎' => 'K', + '𝔏' => 'L', + '𝔐' => 'M', + '𝔑' => 'N', + '𝔒' => 'O', + '𝔓' => 'P', + '𝔔' => 'Q', + '𝔖' => 'S', + '𝔗' => 'T', + '𝔘' => 'U', + '𝔙' => 'V', + '𝔚' => 'W', + '𝔛' => 'X', + '𝔜' => 'Y', + '𝔞' => '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', + '𝔸' => 'A', + '𝔹' => 'B', + '𝔻' => 'D', + '𝔼' => 'E', + '𝔽' => 'F', + '𝔾' => 'G', + '𝕀' => 'I', + '𝕁' => 'J', + '𝕂' => 'K', + '𝕃' => 'L', + '𝕄' => 'M', + '𝕆' => 'O', + '𝕊' => 'S', + '𝕋' => 'T', + '𝕌' => 'U', + '𝕍' => 'V', + '𝕎' => 'W', + '𝕏' => 'X', + '𝕐' => 'Y', + '𝕒' => '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', + '𝕬' => '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', + '𝖆' => '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', + '𝖠' => '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', + '𝖺' => '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', + '𝗔' => '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', + '𝗮' => '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', + '𝘈' => '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', + '𝘢' => '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', + '𝘼' => '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', + '𝙖' => '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', + '𝙰' => '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', + '𝚊' => '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', + '𝚤' => 'ı', + '𝚥' => 'ȷ', + '𝚨' => 'Α', + '𝚩' => 'Β', + '𝚪' => 'Γ', + '𝚫' => 'Δ', + '𝚬' => 'Ε', + '𝚭' => 'Ζ', + '𝚮' => 'Η', + '𝚯' => 'Θ', + '𝚰' => 'Ι', + '𝚱' => 'Κ', + '𝚲' => 'Λ', + '𝚳' => 'Μ', + '𝚴' => 'Ν', + '𝚵' => 'Ξ', + '𝚶' => 'Ο', + '𝚷' => 'Π', + '𝚸' => 'Ρ', + '𝚹' => 'Θ', + '𝚺' => 'Σ', + '𝚻' => 'Τ', + '𝚼' => 'Υ', + '𝚽' => 'Φ', + '𝚾' => 'Χ', + '𝚿' => 'Ψ', + '𝛀' => 'Ω', + '𝛁' => '∇', + '𝛂' => 'α', + '𝛃' => 'β', + '𝛄' => 'γ', + '𝛅' => 'δ', + '𝛆' => 'ε', + '𝛇' => 'ζ', + '𝛈' => 'η', + '𝛉' => 'θ', + '𝛊' => 'ι', + '𝛋' => 'κ', + '𝛌' => 'λ', + '𝛍' => 'μ', + '𝛎' => 'ν', + '𝛏' => 'ξ', + '𝛐' => 'ο', + '𝛑' => 'π', + '𝛒' => 'ρ', + '𝛓' => 'ς', + '𝛔' => 'σ', + '𝛕' => 'τ', + '𝛖' => 'υ', + '𝛗' => 'φ', + '𝛘' => 'χ', + '𝛙' => 'ψ', + '𝛚' => 'ω', + '𝛛' => '∂', + '𝛜' => 'ε', + '𝛝' => 'θ', + '𝛞' => 'κ', + '𝛟' => 'φ', + '𝛠' => 'ρ', + '𝛡' => 'π', + '𝛢' => 'Α', + '𝛣' => 'Β', + '𝛤' => 'Γ', + '𝛥' => 'Δ', + '𝛦' => 'Ε', + '𝛧' => 'Ζ', + '𝛨' => 'Η', + '𝛩' => 'Θ', + '𝛪' => 'Ι', + '𝛫' => 'Κ', + '𝛬' => 'Λ', + '𝛭' => 'Μ', + '𝛮' => 'Ν', + '𝛯' => 'Ξ', + '𝛰' => 'Ο', + '𝛱' => 'Π', + '𝛲' => 'Ρ', + '𝛳' => 'Θ', + '𝛴' => 'Σ', + '𝛵' => 'Τ', + '𝛶' => 'Υ', + '𝛷' => 'Φ', + '𝛸' => 'Χ', + '𝛹' => 'Ψ', + '𝛺' => 'Ω', + '𝛻' => '∇', + '𝛼' => 'α', + '𝛽' => 'β', + '𝛾' => 'γ', + '𝛿' => 'δ', + '𝜀' => 'ε', + '𝜁' => 'ζ', + '𝜂' => 'η', + '𝜃' => 'θ', + '𝜄' => 'ι', + '𝜅' => 'κ', + '𝜆' => 'λ', + '𝜇' => 'μ', + '𝜈' => 'ν', + '𝜉' => 'ξ', + '𝜊' => 'ο', + '𝜋' => 'π', + '𝜌' => 'ρ', + '𝜍' => 'ς', + '𝜎' => 'σ', + '𝜏' => 'τ', + '𝜐' => 'υ', + '𝜑' => 'φ', + '𝜒' => 'χ', + '𝜓' => 'ψ', + '𝜔' => 'ω', + '𝜕' => '∂', + '𝜖' => 'ε', + '𝜗' => 'θ', + '𝜘' => 'κ', + '𝜙' => 'φ', + '𝜚' => 'ρ', + '𝜛' => 'π', + '𝜜' => 'Α', + '𝜝' => 'Β', + '𝜞' => 'Γ', + '𝜟' => 'Δ', + '𝜠' => 'Ε', + '𝜡' => 'Ζ', + '𝜢' => 'Η', + '𝜣' => 'Θ', + '𝜤' => 'Ι', + '𝜥' => 'Κ', + '𝜦' => 'Λ', + '𝜧' => 'Μ', + '𝜨' => 'Ν', + '𝜩' => 'Ξ', + '𝜪' => 'Ο', + '𝜫' => 'Π', + '𝜬' => 'Ρ', + '𝜭' => 'Θ', + '𝜮' => 'Σ', + '𝜯' => 'Τ', + '𝜰' => 'Υ', + '𝜱' => 'Φ', + '𝜲' => 'Χ', + '𝜳' => 'Ψ', + '𝜴' => 'Ω', + '𝜵' => '∇', + '𝜶' => 'α', + '𝜷' => 'β', + '𝜸' => 'γ', + '𝜹' => 'δ', + '𝜺' => 'ε', + '𝜻' => 'ζ', + '𝜼' => 'η', + '𝜽' => 'θ', + '𝜾' => 'ι', + '𝜿' => 'κ', + '𝝀' => 'λ', + '𝝁' => 'μ', + '𝝂' => 'ν', + '𝝃' => 'ξ', + '𝝄' => 'ο', + '𝝅' => 'π', + '𝝆' => 'ρ', + '𝝇' => 'ς', + '𝝈' => 'σ', + '𝝉' => 'τ', + '𝝊' => 'υ', + '𝝋' => 'φ', + '𝝌' => 'χ', + '𝝍' => 'ψ', + '𝝎' => 'ω', + '𝝏' => '∂', + '𝝐' => 'ε', + '𝝑' => 'θ', + '𝝒' => 'κ', + '𝝓' => 'φ', + '𝝔' => 'ρ', + '𝝕' => 'π', + '𝝖' => 'Α', + '𝝗' => 'Β', + '𝝘' => 'Γ', + '𝝙' => 'Δ', + '𝝚' => 'Ε', + '𝝛' => 'Ζ', + '𝝜' => 'Η', + '𝝝' => 'Θ', + '𝝞' => 'Ι', + '𝝟' => 'Κ', + '𝝠' => 'Λ', + '𝝡' => 'Μ', + '𝝢' => 'Ν', + '𝝣' => 'Ξ', + '𝝤' => 'Ο', + '𝝥' => 'Π', + '𝝦' => 'Ρ', + '𝝧' => 'Θ', + '𝝨' => 'Σ', + '𝝩' => 'Τ', + '𝝪' => 'Υ', + '𝝫' => 'Φ', + '𝝬' => 'Χ', + '𝝭' => 'Ψ', + '𝝮' => 'Ω', + '𝝯' => '∇', + '𝝰' => 'α', + '𝝱' => 'β', + '𝝲' => 'γ', + '𝝳' => 'δ', + '𝝴' => 'ε', + '𝝵' => 'ζ', + '𝝶' => 'η', + '𝝷' => 'θ', + '𝝸' => 'ι', + '𝝹' => 'κ', + '𝝺' => 'λ', + '𝝻' => 'μ', + '𝝼' => 'ν', + '𝝽' => 'ξ', + '𝝾' => 'ο', + '𝝿' => 'π', + '𝞀' => 'ρ', + '𝞁' => 'ς', + '𝞂' => 'σ', + '𝞃' => 'τ', + '𝞄' => 'υ', + '𝞅' => 'φ', + '𝞆' => 'χ', + '𝞇' => 'ψ', + '𝞈' => 'ω', + '𝞉' => '∂', + '𝞊' => 'ε', + '𝞋' => 'θ', + '𝞌' => 'κ', + '𝞍' => 'φ', + '𝞎' => 'ρ', + '𝞏' => 'π', + '𝞐' => 'Α', + '𝞑' => 'Β', + '𝞒' => 'Γ', + '𝞓' => 'Δ', + '𝞔' => 'Ε', + '𝞕' => 'Ζ', + '𝞖' => 'Η', + '𝞗' => 'Θ', + '𝞘' => 'Ι', + '𝞙' => 'Κ', + '𝞚' => 'Λ', + '𝞛' => 'Μ', + '𝞜' => 'Ν', + '𝞝' => 'Ξ', + '𝞞' => 'Ο', + '𝞟' => 'Π', + '𝞠' => 'Ρ', + '𝞡' => 'Θ', + '𝞢' => 'Σ', + '𝞣' => 'Τ', + '𝞤' => 'Υ', + '𝞥' => 'Φ', + '𝞦' => 'Χ', + '𝞧' => 'Ψ', + '𝞨' => 'Ω', + '𝞩' => '∇', + '𝞪' => 'α', + '𝞫' => 'β', + '𝞬' => 'γ', + '𝞭' => 'δ', + '𝞮' => 'ε', + '𝞯' => 'ζ', + '𝞰' => 'η', + '𝞱' => 'θ', + '𝞲' => 'ι', + '𝞳' => 'κ', + '𝞴' => 'λ', + '𝞵' => 'μ', + '𝞶' => 'ν', + '𝞷' => 'ξ', + '𝞸' => 'ο', + '𝞹' => 'π', + '𝞺' => 'ρ', + '𝞻' => 'ς', + '𝞼' => 'σ', + '𝞽' => 'τ', + '𝞾' => 'υ', + '𝞿' => 'φ', + '𝟀' => 'χ', + '𝟁' => 'ψ', + '𝟂' => 'ω', + '𝟃' => '∂', + '𝟄' => 'ε', + '𝟅' => 'θ', + '𝟆' => 'κ', + '𝟇' => 'φ', + '𝟈' => 'ρ', + '𝟉' => 'π', + '𝟊' => 'Ϝ', + '𝟋' => 'ϝ', + '𝟎' => '0', + '𝟏' => '1', + '𝟐' => '2', + '𝟑' => '3', + '𝟒' => '4', + '𝟓' => '5', + '𝟔' => '6', + '𝟕' => '7', + '𝟖' => '8', + '𝟗' => '9', + '𝟘' => '0', + '𝟙' => '1', + '𝟚' => '2', + '𝟛' => '3', + '𝟜' => '4', + '𝟝' => '5', + '𝟞' => '6', + '𝟟' => '7', + '𝟠' => '8', + '𝟡' => '9', + '𝟢' => '0', + '𝟣' => '1', + '𝟤' => '2', + '𝟥' => '3', + '𝟦' => '4', + '𝟧' => '5', + '𝟨' => '6', + '𝟩' => '7', + '𝟪' => '8', + '𝟫' => '9', + '𝟬' => '0', + '𝟭' => '1', + '𝟮' => '2', + '𝟯' => '3', + '𝟰' => '4', + '𝟱' => '5', + '𝟲' => '6', + '𝟳' => '7', + '𝟴' => '8', + '𝟵' => '9', + '𝟶' => '0', + '𝟷' => '1', + '𝟸' => '2', + '𝟹' => '3', + '𝟺' => '4', + '𝟻' => '5', + '𝟼' => '6', + '𝟽' => '7', + '𝟾' => '8', + '𝟿' => '9', + '𞸀' => 'ا', + '𞸁' => 'ب', + '𞸂' => 'ج', + '𞸃' => 'د', + '𞸅' => 'و', + '𞸆' => 'ز', + '𞸇' => 'ح', + '𞸈' => 'ط', + '𞸉' => 'ي', + '𞸊' => 'ك', + '𞸋' => 'ل', + '𞸌' => 'م', + '𞸍' => 'ن', + '𞸎' => 'س', + '𞸏' => 'ع', + '𞸐' => 'ف', + '𞸑' => 'ص', + '𞸒' => 'ق', + '𞸓' => 'ر', + '𞸔' => 'ش', + '𞸕' => 'ت', + '𞸖' => 'ث', + '𞸗' => 'خ', + '𞸘' => 'ذ', + '𞸙' => 'ض', + '𞸚' => 'ظ', + '𞸛' => 'غ', + '𞸜' => 'ٮ', + '𞸝' => 'ں', + '𞸞' => 'ڡ', + '𞸟' => 'ٯ', + '𞸡' => 'ب', + '𞸢' => 'ج', + '𞸤' => 'ه', + '𞸧' => 'ح', + '𞸩' => 'ي', + '𞸪' => 'ك', + '𞸫' => 'ل', + '𞸬' => 'م', + '𞸭' => 'ن', + '𞸮' => 'س', + '𞸯' => 'ع', + '𞸰' => 'ف', + '𞸱' => 'ص', + '𞸲' => 'ق', + '𞸴' => 'ش', + '𞸵' => 'ت', + '𞸶' => 'ث', + '𞸷' => 'خ', + '𞸹' => 'ض', + '𞸻' => 'غ', + '𞹂' => 'ج', + '𞹇' => 'ح', + '𞹉' => 'ي', + '𞹋' => 'ل', + '𞹍' => 'ن', + '𞹎' => 'س', + '𞹏' => 'ع', + '𞹑' => 'ص', + '𞹒' => 'ق', + '𞹔' => 'ش', + '𞹗' => 'خ', + '𞹙' => 'ض', + '𞹛' => 'غ', + '𞹝' => 'ں', + '𞹟' => 'ٯ', + '𞹡' => 'ب', + '𞹢' => 'ج', + '𞹤' => 'ه', + '𞹧' => 'ح', + '𞹨' => 'ط', + '𞹩' => 'ي', + '𞹪' => 'ك', + '𞹬' => 'م', + '𞹭' => 'ن', + '𞹮' => 'س', + '𞹯' => 'ع', + '𞹰' => 'ف', + '𞹱' => 'ص', + '𞹲' => 'ق', + '𞹴' => 'ش', + '𞹵' => 'ت', + '𞹶' => 'ث', + '𞹷' => 'خ', + '𞹹' => 'ض', + '𞹺' => 'ظ', + '𞹻' => 'غ', + '𞹼' => 'ٮ', + '𞹾' => 'ڡ', + '𞺀' => 'ا', + '𞺁' => 'ب', + '𞺂' => 'ج', + '𞺃' => 'د', + '𞺄' => 'ه', + '𞺅' => 'و', + '𞺆' => 'ز', + '𞺇' => 'ح', + '𞺈' => 'ط', + '𞺉' => 'ي', + '𞺋' => 'ل', + '𞺌' => 'م', + '𞺍' => 'ن', + '𞺎' => 'س', + '𞺏' => 'ع', + '𞺐' => 'ف', + '𞺑' => 'ص', + '𞺒' => 'ق', + '𞺓' => 'ر', + '𞺔' => 'ش', + '𞺕' => 'ت', + '𞺖' => 'ث', + '𞺗' => 'خ', + '𞺘' => 'ذ', + '𞺙' => 'ض', + '𞺚' => 'ظ', + '𞺛' => 'غ', + '𞺡' => 'ب', + '𞺢' => 'ج', + '𞺣' => 'د', + '𞺥' => 'و', + '𞺦' => 'ز', + '𞺧' => 'ح', + '𞺨' => 'ط', + '𞺩' => 'ي', + '𞺫' => 'ل', + '𞺬' => 'م', + '𞺭' => 'ن', + '𞺮' => 'س', + '𞺯' => 'ع', + '𞺰' => 'ف', + '𞺱' => 'ص', + '𞺲' => 'ق', + '𞺳' => 'ر', + '𞺴' => 'ش', + '𞺵' => 'ت', + '𞺶' => 'ث', + '𞺷' => 'خ', + '𞺸' => 'ذ', + '𞺹' => 'ض', + '𞺺' => 'ظ', + '𞺻' => 'غ', + '🄀' => '0.', + '🄁' => '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)', + '🄪' => '〔S〕', + '🄫' => 'C', + '🄬' => 'R', + '🄭' => 'CD', + '🄮' => 'WZ', + '🄰' => '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', + '🅊' => 'HV', + '🅋' => 'MV', + '🅌' => 'SD', + '🅍' => 'SS', + '🅎' => 'PPV', + '🅏' => 'WC', + '🅪' => 'MC', + '🅫' => 'MD', + '🅬' => 'MR', + '🆐' => 'DJ', + '🈀' => 'ほか', + '🈁' => 'ココ', + '🈂' => 'サ', + '🈐' => '手', + '🈑' => '字', + '🈒' => '双', + '🈓' => 'デ', + '🈔' => '二', + '🈕' => '多', + '🈖' => '解', + '🈗' => '天', + '🈘' => '交', + '🈙' => '映', + '🈚' => '無', + '🈛' => '料', + '🈜' => '前', + '🈝' => '後', + '🈞' => '再', + '🈟' => '新', + '🈠' => '初', + '🈡' => '終', + '🈢' => '生', + '🈣' => '販', + '🈤' => '声', + '🈥' => '吹', + '🈦' => '演', + '🈧' => '投', + '🈨' => '捕', + '🈩' => '一', + '🈪' => '三', + '🈫' => '遊', + '🈬' => '左', + '🈭' => '中', + '🈮' => '右', + '🈯' => '指', + '🈰' => '走', + '🈱' => '打', + '🈲' => '禁', + '🈳' => '空', + '🈴' => '合', + '🈵' => '満', + '🈶' => '有', + '🈷' => '月', + '🈸' => '申', + '🈹' => '割', + '🈺' => '営', + '🈻' => '配', + '🉀' => '〔本〕', + '🉁' => '〔三〕', + '🉂' => '〔二〕', + '🉃' => '〔安〕', + '🉄' => '〔点〕', + '🉅' => '〔打〕', + '🉆' => '〔盗〕', + '🉇' => '〔勝〕', + '🉈' => '〔敗〕', + '🉐' => '得', + '🉑' => '可', + '🯰' => '0', + '🯱' => '1', + '🯲' => '2', + '🯳' => '3', + '🯴' => '4', + '🯵' => '5', + '🯶' => '6', + '🯷' => '7', + '🯸' => '8', + '🯹' => '9', +); diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap.php new file mode 100644 index 0000000..3608e5c --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Normalizer as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('normalizer_is_normalized')) { + function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); } +} +if (!function_exists('normalizer_normalize')) { + function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php b/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php new file mode 100644 index 0000000..e36d1a9 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Normalizer as p; + +if (!function_exists('normalizer_is_normalized')) { + function normalizer_is_normalized(?string $string, ?int $form = p\Normalizer::FORM_C): bool { return p\Normalizer::isNormalized((string) $string, (int) $form); } +} +if (!function_exists('normalizer_normalize')) { + function normalizer_normalize(?string $string, ?int $form = p\Normalizer::FORM_C): string|false { return p\Normalizer::normalize((string) $string, (int) $form); } +} diff --git a/plugins/email/vendor/symfony/polyfill-intl-normalizer/composer.json b/plugins/email/vendor/symfony/polyfill-intl-normalizer/composer.json new file mode 100644 index 0000000..eb452bf --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-intl-normalizer/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/polyfill-intl-normalizer", + "type": "library", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "normalizer"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/plugins/email/vendor/symfony/polyfill-php80/LICENSE b/plugins/email/vendor/symfony/polyfill-php80/LICENSE new file mode 100644 index 0000000..5593b1d --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/polyfill-php80/Php80.php b/plugins/email/vendor/symfony/polyfill-php80/Php80.php new file mode 100644 index 0000000..362dd1a --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/Php80.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Ion Bazan + * @author Nico Oelgart + * @author Nicolas Grekas + * + * @internal + */ +final class Php80 +{ + public static function fdiv(float $dividend, float $divisor): float + { + return @($dividend / $divisor); + } + + public static function get_debug_type($value): string + { + switch (true) { + case null === $value: return 'null'; + case \is_bool($value): return 'bool'; + case \is_string($value): return 'string'; + case \is_array($value): return 'array'; + case \is_int($value): return 'int'; + case \is_float($value): return 'float'; + case \is_object($value): break; + case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; + default: + if (null === $type = @get_resource_type($value)) { + return 'unknown'; + } + + if ('Unknown' === $type) { + $type = 'closed'; + } + + return "resource ($type)"; + } + + $class = \get_class($value); + + if (false === strpos($class, '@')) { + return $class; + } + + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + } + + public static function get_resource_id($res): int + { + if (!\is_resource($res) && null === @get_resource_type($res)) { + throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); + } + + return (int) $res; + } + + public static function preg_last_error_msg(): string + { + switch (preg_last_error()) { + case \PREG_INTERNAL_ERROR: + return 'Internal error'; + case \PREG_BAD_UTF8_ERROR: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + case \PREG_BAD_UTF8_OFFSET_ERROR: + return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; + case \PREG_BACKTRACK_LIMIT_ERROR: + return 'Backtrack limit exhausted'; + case \PREG_RECURSION_LIMIT_ERROR: + return 'Recursion limit exhausted'; + case \PREG_JIT_STACKLIMIT_ERROR: + return 'JIT stack limit exhausted'; + case \PREG_NO_ERROR: + return 'No error'; + default: + return 'Unknown error'; + } + } + + public static function str_contains(string $haystack, string $needle): bool + { + return '' === $needle || false !== strpos($haystack, $needle); + } + + public static function str_starts_with(string $haystack, string $needle): bool + { + return 0 === strncmp($haystack, $needle, \strlen($needle)); + } + + public static function str_ends_with(string $haystack, string $needle): bool + { + if ('' === $needle || $needle === $haystack) { + return true; + } + + if ('' === $haystack) { + return false; + } + + $needleLength = \strlen($needle); + + return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); + } +} diff --git a/plugins/email/vendor/symfony/polyfill-php80/PhpToken.php b/plugins/email/vendor/symfony/polyfill-php80/PhpToken.php new file mode 100644 index 0000000..fe6e691 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/PhpToken.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Fedonyuk Anton + * + * @internal + */ +class PhpToken implements \Stringable +{ + /** + * @var int + */ + public $id; + + /** + * @var string + */ + public $text; + + /** + * @var int + */ + public $line; + + /** + * @var int + */ + public $pos; + + public function __construct(int $id, string $text, int $line = -1, int $position = -1) + { + $this->id = $id; + $this->text = $text; + $this->line = $line; + $this->pos = $position; + } + + public function getTokenName(): ?string + { + if ('UNKNOWN' === $name = token_name($this->id)) { + $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; + } + + return $name; + } + + /** + * @param int|string|array $kind + */ + public function is($kind): bool + { + foreach ((array) $kind as $value) { + if (\in_array($value, [$this->id, $this->text], true)) { + return true; + } + } + + return false; + } + + public function isIgnorable(): bool + { + return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); + } + + public function __toString(): string + { + return (string) $this->text; + } + + /** + * @return static[] + */ + public static function tokenize(string $code, int $flags = 0): array + { + $line = 1; + $position = 0; + $tokens = token_get_all($code, $flags); + foreach ($tokens as $index => $token) { + if (\is_string($token)) { + $id = \ord($token); + $text = $token; + } else { + [$id, $text, $line] = $token; + } + $tokens[$index] = new static($id, $text, $line, $position); + $position += \strlen($text); + } + + return $tokens; + } +} diff --git a/plugins/email/vendor/symfony/polyfill-php80/README.md b/plugins/email/vendor/symfony/polyfill-php80/README.md new file mode 100644 index 0000000..3816c55 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/README.md @@ -0,0 +1,25 @@ +Symfony Polyfill / Php80 +======================== + +This component provides features added to PHP 8.0 core: + +- [`Stringable`](https://php.net/stringable) interface +- [`fdiv`](https://php.net/fdiv) +- [`ValueError`](https://php.net/valueerror) class +- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class +- `FILTER_VALIDATE_BOOL` constant +- [`get_debug_type`](https://php.net/get_debug_type) +- [`PhpToken`](https://php.net/phptoken) class +- [`preg_last_error_msg`](https://php.net/preg_last_error_msg) +- [`str_contains`](https://php.net/str_contains) +- [`str_starts_with`](https://php.net/str_starts_with) +- [`str_ends_with`](https://php.net/str_ends_with) +- [`get_resource_id`](https://php.net/get_resource_id) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plugins/email/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php b/plugins/email/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php new file mode 100644 index 0000000..7ea6d27 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php @@ -0,0 +1,22 @@ +flags = $flags; + } +} diff --git a/plugins/email/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php b/plugins/email/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php new file mode 100644 index 0000000..72f1081 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php80 as p; + +if (\PHP_VERSION_ID >= 80000) { + return; +} + +if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { + define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); +} + +if (!function_exists('fdiv')) { + function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } +} +if (!function_exists('preg_last_error_msg')) { + function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } +} +if (!function_exists('str_contains')) { + function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_starts_with')) { + function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_ends_with')) { + function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('get_debug_type')) { + function get_debug_type($value): string { return p\Php80::get_debug_type($value); } +} +if (!function_exists('get_resource_id')) { + function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } +} diff --git a/plugins/email/vendor/symfony/polyfill-php80/composer.json b/plugins/email/vendor/symfony/polyfill-php80/composer.json new file mode 100644 index 0000000..cd3e9b6 --- /dev/null +++ b/plugins/email/vendor/symfony/polyfill-php80/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/polyfill-php80", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/plugins/email/vendor/symfony/redis-messenger/CHANGELOG.md b/plugins/email/vendor/symfony/redis-messenger/CHANGELOG.md new file mode 100644 index 0000000..700abd7 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/CHANGELOG.md @@ -0,0 +1,34 @@ +CHANGELOG +========= + +5.4 +--- + + * Deprecate not setting the `delete_after_ack` config option (or DSN parameter), + its default value will change to `true` in 6.0 + +5.3 +--- + + * Add `rediss://` DSN scheme support for TLS protocol + * Deprecate TLS option, use `rediss://127.0.0.1` instead of `redis://127.0.0.1?tls=1` + * Add support for `\RedisCluster` instance in `Connection` constructor + * Add support for Redis Cluster in DSN + +5.2.0 +----- + + * Added a `delete_after_reject` option to the DSN to allow control over message + deletion, similar to `delete_after_ack`. + * Added option `lazy` to delay connecting to Redis server until we first use it. + +5.1.0 +----- + + * Introduced the Redis bridge. + * Added TLS option in the DSN. Example: `redis://127.0.0.1?tls=1` + * Deprecated use of invalid options + * Added ability to receive of old pending messages with new `redeliver_timeout` + and `claim_interval` options. + * Added a `delete_after_ack` option to the DSN as an alternative to + `stream_max_entries` to avoid leaking memory. diff --git a/plugins/email/vendor/symfony/redis-messenger/LICENSE b/plugins/email/vendor/symfony/redis-messenger/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/redis-messenger/README.md b/plugins/email/vendor/symfony/redis-messenger/README.md new file mode 100644 index 0000000..2a56984 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/README.md @@ -0,0 +1,12 @@ +Redis Messenger +=============== + +Provides Redis integration for Symfony Messenger. + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/Connection.php b/plugins/email/vendor/symfony/redis-messenger/Transport/Connection.php new file mode 100644 index 0000000..ffcac3e --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/Connection.php @@ -0,0 +1,596 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Exception\TransportException; + +/** + * A Redis connection. + * + * @author Alexander Schranz + * @author Antoine Bluchet + * @author Robin Chalas + * + * @internal + * @final + */ +class Connection +{ + private const DEFAULT_OPTIONS = [ + 'stream' => 'messages', + 'group' => 'symfony', + 'consumer' => 'consumer', + 'auto_setup' => true, + 'delete_after_ack' => false, + 'delete_after_reject' => true, + 'stream_max_entries' => 0, // any value higher than 0 defines an approximate maximum number of stream entries + 'dbindex' => 0, + 'tls' => false, + 'redeliver_timeout' => 3600, // Timeout before redeliver messages still in pending state (seconds) + 'claim_interval' => 60000, // Interval by which pending/abandoned messages should be checked + 'lazy' => false, + 'auth' => null, + 'serializer' => \Redis::SERIALIZER_PHP, + ]; + + private $connection; + private $stream; + private $queue; + private $group; + private $consumer; + private $autoSetup; + private $maxEntries; + private $redeliverTimeout; + private $nextClaim = 0.0; + private $claimInterval; + private $deleteAfterAck; + private $deleteAfterReject; + private $couldHavePendingMessages = true; + + /** + * @param \Redis|\RedisCluster|null $redis + */ + public function __construct(array $configuration, array $connectionCredentials = [], array $redisOptions = [], $redis = null) + { + if (version_compare(phpversion('redis'), '4.3.0', '<')) { + throw new LogicException('The redis transport requires php-redis 4.3.0 or higher.'); + } + + $host = $connectionCredentials['host'] ?? '127.0.0.1'; + $port = $connectionCredentials['port'] ?? 6379; + $serializer = $redisOptions['serializer'] ?? \Redis::SERIALIZER_PHP; + $dbIndex = $configuration['dbindex'] ?? self::DEFAULT_OPTIONS['dbindex']; + $auth = $connectionCredentials['auth'] ?? null; + if ('' === $auth) { + $auth = null; + } + + $lazy = $configuration['lazy'] ?? self::DEFAULT_OPTIONS['lazy']; + if (\is_array($host) || $redis instanceof \RedisCluster) { + $hosts = \is_string($host) ? [$host.':'.$port] : $host; // Always ensure we have an array + $initializer = static function ($redis) use ($hosts, $auth, $serializer) { + return self::initializeRedisCluster($redis, $hosts, $auth, $serializer); + }; + $redis = $lazy ? new RedisClusterProxy($redis, $initializer) : $initializer($redis); + } else { + $redis = $redis ?? new \Redis(); + $initializer = static function ($redis) use ($host, $port, $auth, $serializer, $dbIndex) { + return self::initializeRedis($redis, $host, $port, $auth, $serializer, $dbIndex); + }; + $redis = $lazy ? new RedisProxy($redis, $initializer) : $initializer($redis); + } + + $this->connection = $redis; + + foreach (['stream', 'group', 'consumer'] as $key) { + if (isset($configuration[$key]) && '' === $configuration[$key]) { + throw new InvalidArgumentException(sprintf('"%s" should be configured, got an empty string.', $key)); + } + } + + $this->stream = $configuration['stream'] ?? self::DEFAULT_OPTIONS['stream']; + $this->group = $configuration['group'] ?? self::DEFAULT_OPTIONS['group']; + $this->consumer = $configuration['consumer'] ?? self::DEFAULT_OPTIONS['consumer']; + $this->queue = $this->stream.'__queue'; + $this->autoSetup = $configuration['auto_setup'] ?? self::DEFAULT_OPTIONS['auto_setup']; + $this->maxEntries = $configuration['stream_max_entries'] ?? self::DEFAULT_OPTIONS['stream_max_entries']; + $this->deleteAfterAck = $configuration['delete_after_ack'] ?? self::DEFAULT_OPTIONS['delete_after_ack']; + $this->deleteAfterReject = $configuration['delete_after_reject'] ?? self::DEFAULT_OPTIONS['delete_after_reject']; + $this->redeliverTimeout = ($configuration['redeliver_timeout'] ?? self::DEFAULT_OPTIONS['redeliver_timeout']) * 1000; + $this->claimInterval = ($configuration['claim_interval'] ?? self::DEFAULT_OPTIONS['claim_interval']) / 1000; + } + + /** + * @param string|string[]|null $auth + */ + private static function initializeRedis(\Redis $redis, string $host, int $port, $auth, int $serializer, int $dbIndex): \Redis + { + $redis->connect($host, $port); + $redis->setOption(\Redis::OPT_SERIALIZER, $serializer); + + if (null !== $auth && !$redis->auth($auth)) { + throw new InvalidArgumentException('Redis connection failed: '.$redis->getLastError()); + } + + if ($dbIndex && !$redis->select($dbIndex)) { + throw new InvalidArgumentException('Redis connection failed: '.$redis->getLastError()); + } + + return $redis; + } + + /** + * @param string|string[]|null $auth + */ + private static function initializeRedisCluster(?\RedisCluster $redis, array $hosts, $auth, int $serializer): \RedisCluster + { + if (null === $redis) { + $redis = new \RedisCluster(null, $hosts, 0.0, 0.0, false, $auth); + } + + $redis->setOption(\Redis::OPT_SERIALIZER, $serializer); + + return $redis; + } + + /** + * @param \Redis|\RedisCluster|null $redis + */ + public static function fromDsn(string $dsn, array $redisOptions = [], $redis = null): self + { + if (false === strpos($dsn, ',')) { + $parsedUrl = self::parseDsn($dsn, $redisOptions); + } else { + $dsns = explode(',', $dsn); + $parsedUrls = array_map(function ($dsn) use (&$redisOptions) { + return self::parseDsn($dsn, $redisOptions); + }, $dsns); + + // Merge all the URLs, the last one overrides the previous ones + $parsedUrl = array_merge(...$parsedUrls); + + // Regroup all the hosts in an array interpretable by RedisCluster + $parsedUrl['host'] = array_map(function ($parsedUrl, $dsn) { + if (!isset($parsedUrl['host'])) { + throw new InvalidArgumentException(sprintf('Missing host in DSN part "%s", it must be defined when using Redis Cluster.', $dsn)); + } + + return $parsedUrl['host'].':'.($parsedUrl['port'] ?? 6379); + }, $parsedUrls, $dsns); + } + + self::validateOptions($redisOptions); + + $autoSetup = null; + if (\array_key_exists('auto_setup', $redisOptions)) { + $autoSetup = filter_var($redisOptions['auto_setup'], \FILTER_VALIDATE_BOOLEAN); + unset($redisOptions['auto_setup']); + } + + $maxEntries = null; + if (\array_key_exists('stream_max_entries', $redisOptions)) { + $maxEntries = filter_var($redisOptions['stream_max_entries'], \FILTER_VALIDATE_INT); + unset($redisOptions['stream_max_entries']); + } + + $deleteAfterAck = null; + if (\array_key_exists('delete_after_ack', $redisOptions)) { + $deleteAfterAck = filter_var($redisOptions['delete_after_ack'], \FILTER_VALIDATE_BOOLEAN); + unset($redisOptions['delete_after_ack']); + } else { + trigger_deprecation('symfony/redis-messenger', '5.4', 'Not setting the "delete_after_ack" boolean option explicitly is deprecated, its default value will change to true in 6.0.'); + } + + $deleteAfterReject = null; + if (\array_key_exists('delete_after_reject', $redisOptions)) { + $deleteAfterReject = filter_var($redisOptions['delete_after_reject'], \FILTER_VALIDATE_BOOLEAN); + unset($redisOptions['delete_after_reject']); + } + + $dbIndex = null; + if (\array_key_exists('dbindex', $redisOptions)) { + $dbIndex = filter_var($redisOptions['dbindex'], \FILTER_VALIDATE_INT); + unset($redisOptions['dbindex']); + } + + $tls = 'rediss' === $parsedUrl['scheme']; + if (\array_key_exists('tls', $redisOptions)) { + trigger_deprecation('symfony/redis-messenger', '5.3', 'Providing "tls" parameter is deprecated, use "rediss://" DSN scheme instead'); + $tls = filter_var($redisOptions['tls'], \FILTER_VALIDATE_BOOLEAN); + unset($redisOptions['tls']); + } + + $redeliverTimeout = null; + if (\array_key_exists('redeliver_timeout', $redisOptions)) { + $redeliverTimeout = filter_var($redisOptions['redeliver_timeout'], \FILTER_VALIDATE_INT); + unset($redisOptions['redeliver_timeout']); + } + + $claimInterval = null; + if (\array_key_exists('claim_interval', $redisOptions)) { + $claimInterval = filter_var($redisOptions['claim_interval'], \FILTER_VALIDATE_INT); + unset($redisOptions['claim_interval']); + } + + $configuration = [ + 'stream' => $redisOptions['stream'] ?? null, + 'group' => $redisOptions['group'] ?? null, + 'consumer' => $redisOptions['consumer'] ?? null, + 'lazy' => $redisOptions['lazy'] ?? self::DEFAULT_OPTIONS['lazy'], + 'auto_setup' => $autoSetup, + 'stream_max_entries' => $maxEntries, + 'delete_after_ack' => $deleteAfterAck, + 'delete_after_reject' => $deleteAfterReject, + 'dbindex' => $dbIndex, + 'redeliver_timeout' => $redeliverTimeout, + 'claim_interval' => $claimInterval, + ]; + + if (isset($parsedUrl['host'])) { + $pass = '' !== ($parsedUrl['pass'] ?? '') ? urldecode($parsedUrl['pass']) : null; + $user = '' !== ($parsedUrl['user'] ?? '') ? urldecode($parsedUrl['user']) : null; + $connectionCredentials = [ + 'host' => $parsedUrl['host'] ?? '127.0.0.1', + 'port' => $parsedUrl['port'] ?? 6379, + // See: https://github.com/phpredis/phpredis/#auth + 'auth' => $redisOptions['auth'] ?? (null !== $pass && null !== $user ? [$user, $pass] : ($pass ?? $user)), + ]; + + $pathParts = explode('/', rtrim($parsedUrl['path'] ?? '', '/')); + + $configuration['stream'] = $pathParts[1] ?? $configuration['stream']; + $configuration['group'] = $pathParts[2] ?? $configuration['group']; + $configuration['consumer'] = $pathParts[3] ?? $configuration['consumer']; + if ($tls) { + $connectionCredentials['host'] = 'tls://'.$connectionCredentials['host']; + } + } else { + $connectionCredentials = [ + 'host' => $parsedUrl['path'], + 'port' => 0, + ]; + } + + return new self($configuration, $connectionCredentials, $redisOptions, $redis); + } + + private static function parseDsn(string $dsn, array &$redisOptions): array + { + $url = $dsn; + $scheme = 0 === strpos($dsn, 'rediss:') ? 'rediss' : 'redis'; + + if (preg_match('#^'.$scheme.':///([^:@])+$#', $dsn)) { + $url = str_replace($scheme.':', 'file:', $dsn); + } + + if (false === $parsedUrl = parse_url($url)) { + throw new InvalidArgumentException(sprintf('The given Redis DSN "%s" is invalid.', $dsn)); + } + if (isset($parsedUrl['query'])) { + parse_str($parsedUrl['query'], $dsnOptions); + $redisOptions = array_merge($redisOptions, $dsnOptions); + } + + return $parsedUrl; + } + + private static function validateOptions(array $options): void + { + $availableOptions = array_keys(self::DEFAULT_OPTIONS); + + if (0 < \count($invalidOptions = array_diff(array_keys($options), $availableOptions))) { + trigger_deprecation('symfony/messenger', '5.1', 'Invalid option(s) "%s" passed to the Redis Messenger transport. Passing invalid options is deprecated.', implode('", "', $invalidOptions)); + } + } + + private function claimOldPendingMessages() + { + try { + // This could soon be optimized with https://github.com/antirez/redis/issues/5212 or + // https://github.com/antirez/redis/issues/6256 + $pendingMessages = $this->connection->xpending($this->stream, $this->group, '-', '+', 1); + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + $claimableIds = []; + foreach ($pendingMessages as $pendingMessage) { + if ($pendingMessage[1] === $this->consumer) { + $this->couldHavePendingMessages = true; + + return; + } + + if ($pendingMessage[2] >= $this->redeliverTimeout) { + $claimableIds[] = $pendingMessage[0]; + } + } + + if (\count($claimableIds) > 0) { + try { + $this->connection->xclaim( + $this->stream, + $this->group, + $this->consumer, + $this->redeliverTimeout, + $claimableIds, + ['JUSTID'] + ); + + $this->couldHavePendingMessages = true; + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + } + + $this->nextClaim = microtime(true) + $this->claimInterval; + } + + public function get(): ?array + { + if ($this->autoSetup) { + $this->setup(); + } + $now = microtime(); + $now = substr($now, 11).substr($now, 2, 3); + + $queuedMessageCount = $this->rawCommand('ZCOUNT', 0, $now); + + while ($queuedMessageCount--) { + if (!$message = $this->rawCommand('ZPOPMIN', 1)) { + break; + } + + [$queuedMessage, $expiry] = $message; + + if (\strlen($expiry) === \strlen($now) ? $expiry > $now : \strlen($expiry) < \strlen($now)) { + // if a future-placed message is popped because of a race condition with + // another running consumer, the message is readded to the queue + + if (!$this->rawCommand('ZADD', 'NX', $expiry, $queuedMessage)) { + throw new TransportException('Could not add a message to the redis stream.'); + } + + break; + } + + $decodedQueuedMessage = json_decode($queuedMessage, true); + $this->add(\array_key_exists('body', $decodedQueuedMessage) ? $decodedQueuedMessage['body'] : $queuedMessage, $decodedQueuedMessage['headers'] ?? [], 0); + } + + if (!$this->couldHavePendingMessages && $this->nextClaim <= microtime(true)) { + $this->claimOldPendingMessages(); + } + + $messageId = '>'; // will receive new messages + + if ($this->couldHavePendingMessages) { + $messageId = '0'; // will receive consumers pending messages + } + + try { + $messages = $this->connection->xreadgroup( + $this->group, + $this->consumer, + [$this->stream => $messageId], + 1 + ); + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + if (false === $messages) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + + throw new TransportException($error ?? 'Could not read messages from the redis stream.'); + } + + if ($this->couldHavePendingMessages && empty($messages[$this->stream])) { + $this->couldHavePendingMessages = false; + + // No pending messages so get a new one + return $this->get(); + } + + foreach ($messages[$this->stream] ?? [] as $key => $message) { + return [ + 'id' => $key, + 'data' => $message, + ]; + } + + return null; + } + + public function ack(string $id): void + { + try { + $acknowledged = $this->connection->xack($this->stream, $this->group, [$id]); + if ($this->deleteAfterAck) { + $acknowledged = $this->connection->xdel($this->stream, [$id]); + } + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + if (!$acknowledged) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + throw new TransportException($error ?? sprintf('Could not acknowledge redis message "%s".', $id)); + } + } + + public function reject(string $id): void + { + try { + $deleted = $this->connection->xack($this->stream, $this->group, [$id]); + if ($this->deleteAfterReject) { + $deleted = $this->connection->xdel($this->stream, [$id]) && $deleted; + } + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + if (!$deleted) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + throw new TransportException($error ?? sprintf('Could not delete message "%s" from the redis stream.', $id)); + } + } + + public function add(string $body, array $headers, int $delayInMs = 0): void + { + if ($this->autoSetup) { + $this->setup(); + } + + try { + if ($delayInMs > 0) { // the delay is <= 0 for queued messages + $message = json_encode([ + 'body' => $body, + 'headers' => $headers, + // Entry need to be unique in the sorted set else it would only be added once to the delayed messages queue + 'uniqid' => uniqid('', true), + ]); + + if (false === $message) { + throw new TransportException(json_last_error_msg()); + } + + $now = explode(' ', microtime(), 2); + $now[0] = str_pad($delayInMs + substr($now[0], 2, 3), 3, '0', \STR_PAD_LEFT); + if (3 < \strlen($now[0])) { + $now[1] += substr($now[0], 0, -3); + $now[0] = substr($now[0], -3); + + if (\is_float($now[1])) { + throw new TransportException("Message delay is too big: {$delayInMs}ms."); + } + } + + $added = $this->rawCommand('ZADD', 'NX', $now[1].$now[0], $message); + } else { + $message = json_encode([ + 'body' => $body, + 'headers' => $headers, + ]); + + if (false === $message) { + throw new TransportException(json_last_error_msg()); + } + + if ($this->maxEntries) { + $added = $this->connection->xadd($this->stream, '*', ['message' => $message], $this->maxEntries, true); + } else { + $added = $this->connection->xadd($this->stream, '*', ['message' => $message]); + } + } + } catch (\RedisException $e) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + throw new TransportException($error ?? $e->getMessage(), 0, $e); + } + + if (!$added) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + throw new TransportException($error ?? 'Could not add a message to the redis stream.'); + } + } + + public function setup(): void + { + try { + $this->connection->xgroup('CREATE', $this->stream, $this->group, 0, true); + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + // group might already exist, ignore + if ($this->connection->getLastError()) { + $this->connection->clearLastError(); + } + + if ($this->deleteAfterAck || $this->deleteAfterReject) { + $groups = $this->connection->xinfo('GROUPS', $this->stream); + if ( + // support for Redis extension version 5+ + (\is_array($groups) && 1 < \count($groups)) + // support for Redis extension version 4.x + || (\is_string($groups) && substr_count($groups, '"name"')) + ) { + throw new LogicException(sprintf('More than one group exists for stream "%s", delete_after_ack and delete_after_reject cannot be enabled as it risks deleting messages before all groups could consume them.', $this->stream)); + } + } + + $this->autoSetup = false; + } + + private function getCurrentTimeInMilliseconds(): int + { + return (int) (microtime(true) * 1000); + } + + public function cleanup(): void + { + static $unlink = true; + + if ($unlink) { + try { + $unlink = false !== $this->connection->unlink($this->stream, $this->queue); + } catch (\Throwable $e) { + $unlink = false; + } + } + + if (!$unlink) { + $this->connection->del($this->stream, $this->queue); + } + } + + /** + * @return mixed + */ + private function rawCommand(string $command, ...$arguments) + { + try { + if ($this->connection instanceof \RedisCluster || $this->connection instanceof RedisClusterProxy) { + $result = $this->connection->rawCommand($this->queue, $command, $this->queue, ...$arguments); + } else { + $result = $this->connection->rawCommand($command, $this->queue, ...$arguments); + } + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + if (false === $result) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + throw new TransportException($error ?? sprintf('Could not run "%s" on Redis queue.', $command)); + } + + return $result; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\Connection::class, false)) { + class_alias(Connection::class, \Symfony\Component\Messenger\Transport\RedisExt\Connection::class); +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisClusterProxy.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisClusterProxy.php new file mode 100644 index 0000000..ccbdf77 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisClusterProxy.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +/** + * Allow to delay connection to Redis Cluster. + * + * @author Johann Pardanaud + * + * @internal + */ +class RedisClusterProxy +{ + private $redis; + private $initializer; + private $ready = false; + + public function __construct(?\RedisCluster $redis, \Closure $initializer) + { + $this->redis = $redis; + $this->initializer = $initializer; + } + + public function __call(string $method, array $args) + { + if (!$this->ready) { + $this->redis = $this->initializer->__invoke($this->redis); + $this->ready = true; + } + + return $this->redis->{$method}(...$args); + } +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisProxy.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisProxy.php new file mode 100644 index 0000000..6ad5ecb --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisProxy.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +/** + * Allow to delay connection to Redis. + * + * @author Tobias Nyholm + * @author Nicolas Grekas + * + * @internal + */ +class RedisProxy +{ + private $redis; + private $initializer; + private $ready = false; + + public function __construct(\Redis $redis, \Closure $initializer) + { + $this->redis = $redis; + $this->initializer = $initializer; + } + + public function __call(string $method, array $args) + { + if (!$this->ready) { + $this->redis = $this->initializer->__invoke($this->redis); + $this->ready = true; + } + + return $this->redis->{$method}(...$args); + } +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisReceivedStamp.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisReceivedStamp.php new file mode 100644 index 0000000..3469f69 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisReceivedStamp.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; + +/** + * @author Alexander Schranz + */ +class RedisReceivedStamp implements NonSendableStampInterface +{ + private $id; + + public function __construct(string $id) + { + $this->id = $id; + } + + public function getId(): string + { + return $this->id; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisReceivedStamp::class, false)) { + class_alias(RedisReceivedStamp::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisReceivedStamp::class); +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisReceiver.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisReceiver.php new file mode 100644 index 0000000..1a63d33 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisReceiver.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * @author Alexander Schranz + * @author Antoine Bluchet + */ +class RedisReceiver implements ReceiverInterface +{ + private $connection; + private $serializer; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + $message = $this->connection->get(); + + if (null === $message) { + return []; + } + + $redisEnvelope = json_decode($message['data']['message'] ?? '', true); + + if (null === $redisEnvelope) { + return []; + } + + try { + if (\array_key_exists('body', $redisEnvelope) && \array_key_exists('headers', $redisEnvelope)) { + $envelope = $this->serializer->decode([ + 'body' => $redisEnvelope['body'], + 'headers' => $redisEnvelope['headers'], + ]); + } else { + $envelope = $this->serializer->decode($redisEnvelope); + } + } catch (MessageDecodingFailedException $exception) { + $this->connection->reject($message['id']); + + throw $exception; + } + + return [$envelope->with(new RedisReceivedStamp($message['id']))]; + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + $this->connection->ack($this->findRedisReceivedStamp($envelope)->getId()); + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + $this->connection->reject($this->findRedisReceivedStamp($envelope)->getId()); + } + + private function findRedisReceivedStamp(Envelope $envelope): RedisReceivedStamp + { + /** @var RedisReceivedStamp|null $redisReceivedStamp */ + $redisReceivedStamp = $envelope->last(RedisReceivedStamp::class); + + if (null === $redisReceivedStamp) { + throw new LogicException('No RedisReceivedStamp found on the Envelope.'); + } + + return $redisReceivedStamp; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisReceiver::class, false)) { + class_alias(RedisReceiver::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisReceiver::class); +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisSender.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisSender.php new file mode 100644 index 0000000..433cfe9 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisSender.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\DelayStamp; +use Symfony\Component\Messenger\Transport\Sender\SenderInterface; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + +/** + * @author Alexander Schranz + * @author Antoine Bluchet + */ +class RedisSender implements SenderInterface +{ + private $connection; + private $serializer; + + public function __construct(Connection $connection, SerializerInterface $serializer) + { + $this->connection = $connection; + $this->serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + $encodedMessage = $this->serializer->encode($envelope); + + /** @var DelayStamp|null $delayStamp */ + $delayStamp = $envelope->last(DelayStamp::class); + $delayInMs = null !== $delayStamp ? $delayStamp->getDelay() : 0; + + $this->connection->add($encodedMessage['body'], $encodedMessage['headers'] ?? [], $delayInMs); + + return $envelope; + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisSender::class, false)) { + class_alias(RedisSender::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisSender::class); +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisTransport.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisTransport.php new file mode 100644 index 0000000..88daa22 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisTransport.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\SetupableTransportInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Alexander Schranz + * @author Antoine Bluchet + */ +class RedisTransport implements TransportInterface, SetupableTransportInterface +{ + private $serializer; + private $connection; + private $receiver; + private $sender; + + public function __construct(Connection $connection, SerializerInterface $serializer = null) + { + $this->connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** + * {@inheritdoc} + */ + public function get(): iterable + { + return ($this->receiver ?? $this->getReceiver())->get(); + } + + /** + * {@inheritdoc} + */ + public function ack(Envelope $envelope): void + { + ($this->receiver ?? $this->getReceiver())->ack($envelope); + } + + /** + * {@inheritdoc} + */ + public function reject(Envelope $envelope): void + { + ($this->receiver ?? $this->getReceiver())->reject($envelope); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): Envelope + { + return ($this->sender ?? $this->getSender())->send($envelope); + } + + /** + * {@inheritdoc} + */ + public function setup(): void + { + $this->connection->setup(); + } + + private function getReceiver(): RedisReceiver + { + return $this->receiver = new RedisReceiver($this->connection, $this->serializer); + } + + private function getSender(): RedisSender + { + return $this->sender = new RedisSender($this->connection, $this->serializer); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransport::class, false)) { + class_alias(RedisTransport::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisTransport::class); +} diff --git a/plugins/email/vendor/symfony/redis-messenger/Transport/RedisTransportFactory.php b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisTransportFactory.php new file mode 100644 index 0000000..88cead8 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/Transport/RedisTransportFactory.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Bridge\Redis\Transport; + +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\TransportFactoryInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Alexander Schranz + * @author Antoine Bluchet + */ +class RedisTransportFactory implements TransportFactoryInterface +{ + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + unset($options['transport_name']); + + return new RedisTransport(Connection::fromDsn($dsn, $options), $serializer); + } + + public function supports(string $dsn, array $options): bool + { + return 0 === strpos($dsn, 'redis://') || 0 === strpos($dsn, 'rediss://'); + } +} + +if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class, false)) { + class_alias(RedisTransportFactory::class, \Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class); +} diff --git a/plugins/email/vendor/symfony/redis-messenger/composer.json b/plugins/email/vendor/symfony/redis-messenger/composer.json new file mode 100644 index 0000000..c663be4 --- /dev/null +++ b/plugins/email/vendor/symfony/redis-messenger/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/redis-messenger", + "type": "symfony-messenger-bridge", + "description": "Symfony Redis extension Messenger Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/messenger": "^5.1|^6.0" + }, + "require-dev": { + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Messenger\\Bridge\\Redis\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/plugins/email/vendor/symfony/service-contracts/.gitignore b/plugins/email/vendor/symfony/service-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/plugins/email/vendor/symfony/service-contracts/Attribute/Required.php b/plugins/email/vendor/symfony/service-contracts/Attribute/Required.php new file mode 100644 index 0000000..9df8511 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/Attribute/Required.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Attribute; + +/** + * A required dependency. + * + * This attribute indicates that a property holds a required dependency. The annotated property or method should be + * considered during the instantiation process of the containing class. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] +final class Required +{ +} diff --git a/plugins/email/vendor/symfony/service-contracts/Attribute/SubscribedService.php b/plugins/email/vendor/symfony/service-contracts/Attribute/SubscribedService.php new file mode 100644 index 0000000..10d1bc3 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/Attribute/SubscribedService.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Attribute; + +use Symfony\Contracts\Service\ServiceSubscriberTrait; + +/** + * Use with {@see ServiceSubscriberTrait} to mark a method's return type + * as a subscribed service. + * + * @author Kevin Bond + */ +#[\Attribute(\Attribute::TARGET_METHOD)] +final class SubscribedService +{ + /** + * @param string|null $key The key to use for the service + * If null, use "ClassName::methodName" + */ + public function __construct( + public ?string $key = null + ) { + } +} diff --git a/plugins/email/vendor/symfony/service-contracts/CHANGELOG.md b/plugins/email/vendor/symfony/service-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/plugins/email/vendor/symfony/service-contracts/LICENSE b/plugins/email/vendor/symfony/service-contracts/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/email/vendor/symfony/service-contracts/README.md b/plugins/email/vendor/symfony/service-contracts/README.md new file mode 100644 index 0000000..41e054a --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/README.md @@ -0,0 +1,9 @@ +Symfony Service Contracts +========================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/plugins/email/vendor/symfony/service-contracts/ResetInterface.php b/plugins/email/vendor/symfony/service-contracts/ResetInterface.php new file mode 100644 index 0000000..1af1075 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/ResetInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * Provides a way to reset an object to its initial state. + * + * When calling the "reset()" method on an object, it should be put back to its + * initial state. This usually means clearing any internal buffers and forwarding + * the call to internal dependencies. All properties of the object should be put + * back to the same state it had when it was first ready to use. + * + * This method could be called, for example, to recycle objects that are used as + * services, so that they can be used to handle several requests in the same + * process loop (note that we advise making your services stateless instead of + * implementing this interface when possible.) + */ +interface ResetInterface +{ + public function reset(); +} diff --git a/plugins/email/vendor/symfony/service-contracts/ServiceLocatorTrait.php b/plugins/email/vendor/symfony/service-contracts/ServiceLocatorTrait.php new file mode 100644 index 0000000..74dfa43 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/ServiceLocatorTrait.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(ContainerExceptionInterface::class); +class_exists(NotFoundExceptionInterface::class); + +/** + * A trait to help implement ServiceProviderInterface. + * + * @author Robin Chalas + * @author Nicolas Grekas + */ +trait ServiceLocatorTrait +{ + private $factories; + private $loading = []; + private $providedTypes; + + /** + * @param callable[] $factories + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function has(string $id) + { + return isset($this->factories[$id]); + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get(string $id) + { + if (!isset($this->factories[$id])) { + throw $this->createNotFoundException($id); + } + + if (isset($this->loading[$id])) { + $ids = array_values($this->loading); + $ids = \array_slice($this->loading, array_search($id, $ids)); + $ids[] = $id; + + throw $this->createCircularReferenceException($id, $ids); + } + + $this->loading[$id] = $id; + try { + return $this->factories[$id]($this); + } finally { + unset($this->loading[$id]); + } + } + + /** + * {@inheritdoc} + */ + public function getProvidedServices(): array + { + if (null === $this->providedTypes) { + $this->providedTypes = []; + + foreach ($this->factories as $name => $factory) { + if (!\is_callable($factory)) { + $this->providedTypes[$name] = '?'; + } else { + $type = (new \ReflectionFunction($factory))->getReturnType(); + + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; + } + } + } + + return $this->providedTypes; + } + + private function createNotFoundException(string $id): NotFoundExceptionInterface + { + if (!$alternatives = array_keys($this->factories)) { + $message = 'is empty...'; + } else { + $last = array_pop($alternatives); + if ($alternatives) { + $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); + } else { + $message = sprintf('only knows about the "%s" service.', $last); + } + } + + if ($this->loading) { + $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); + } else { + $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message); + } + + return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { + }; + } + + private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface + { + return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { + }; + } +} diff --git a/plugins/email/vendor/symfony/service-contracts/ServiceProviderInterface.php b/plugins/email/vendor/symfony/service-contracts/ServiceProviderInterface.php new file mode 100644 index 0000000..c60ad0b --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/ServiceProviderInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. + * + * @author Nicolas Grekas + * @author Mateusz Sip + */ +interface ServiceProviderInterface extends ContainerInterface +{ + /** + * Returns an associative array of service types keyed by the identifiers provided by the current container. + * + * Examples: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface + * * ['foo' => '?'] means the container provides service name "foo" of unspecified type + * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null + * + * @return string[] The provided service types, keyed by service names + */ + public function getProvidedServices(): array; +} diff --git a/plugins/email/vendor/symfony/service-contracts/ServiceSubscriberInterface.php b/plugins/email/vendor/symfony/service-contracts/ServiceSubscriberInterface.php new file mode 100644 index 0000000..098ab90 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/ServiceSubscriberInterface.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. Service types that start with an interrogation + * mark "?" are optional, while the other ones are mandatory service dependencies. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + * + * @author Nicolas Grekas + */ +interface ServiceSubscriberInterface +{ + /** + * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * + * For mandatory dependencies: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name + * internally to fetch an iterable of Psr\Log\LoggerInterface instances. + * * ['Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] + * + * otherwise: + * + * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency + * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency + * * ['?Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] + * + * @return string[] The required service types, optionally keyed by service names + */ + public static function getSubscribedServices(); +} diff --git a/plugins/email/vendor/symfony/service-contracts/ServiceSubscriberTrait.php b/plugins/email/vendor/symfony/service-contracts/ServiceSubscriberTrait.php new file mode 100644 index 0000000..16e3eb2 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/ServiceSubscriberTrait.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\SubscribedService; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services from + * method return types. Service ids are available as "ClassName::methodName". + * + * @author Kevin Bond + */ +trait ServiceSubscriberTrait +{ + /** @var ContainerInterface */ + protected $container; + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices(): array + { + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + $attributeOptIn = false; + + if (\PHP_VERSION_ID >= 80000) { + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } + + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } + + $serviceId = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + + if ($returnType->allowsNull()) { + $serviceId = '?'.$serviceId; + } + + $services[$attribute->newInstance()->key ?? self::class.'::'.$method->name] = $serviceId; + $attributeOptIn = true; + } + } + + if (!$attributeOptIn) { + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + continue; + } + + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!($returnType = $method->getReturnType()) instanceof \ReflectionNamedType) { + continue; + } + + if ($returnType->isBuiltin()) { + continue; + } + + if (\PHP_VERSION_ID >= 80000) { + trigger_deprecation('symfony/service-contracts', '2.5', 'Using "%s" in "%s" without using the "%s" attribute on any method is deprecated.', ServiceSubscriberTrait::class, self::class, SubscribedService::class); + } + + $services[self::class.'::'.$method->name] = '?'.($returnType instanceof \ReflectionNamedType ? $returnType->getName() : $returnType); + } + } + + return $services; + } + + /** + * @required + * + * @return ContainerInterface|null + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { + return parent::setContainer($container); + } + + return null; + } +} diff --git a/plugins/email/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php b/plugins/email/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php new file mode 100644 index 0000000..2a1b565 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Test; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\ServiceLocatorTrait; + +abstract class ServiceLocatorTest extends TestCase +{ + /** + * @return ContainerInterface + */ + protected function getServiceLocator(array $factories) + { + return new class($factories) implements ContainerInterface { + use ServiceLocatorTrait; + }; + } + + public function testHas() + { + $locator = $this->getServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + function () { return 'dummy'; }, + ]); + + $this->assertTrue($locator->has('foo')); + $this->assertTrue($locator->has('bar')); + $this->assertFalse($locator->has('dummy')); + } + + public function testGet() + { + $locator = $this->getServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + ]); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('baz', $locator->get('bar')); + } + + public function testGetDoesNotMemoize() + { + $i = 0; + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$i) { + ++$i; + + return 'bar'; + }, + ]); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame(2, $i); + } + + public function testThrowsOnUndefinedInternalService() + { + if (!$this->getExpectedException()) { + $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); + } + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$locator) { return $locator->get('bar'); }, + ]); + + $locator->get('foo'); + } + + public function testThrowsOnCircularReference() + { + $this->expectException(\Psr\Container\ContainerExceptionInterface::class); + $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$locator) { return $locator->get('bar'); }, + 'bar' => function () use (&$locator) { return $locator->get('baz'); }, + 'baz' => function () use (&$locator) { return $locator->get('bar'); }, + ]); + + $locator->get('foo'); + } +} diff --git a/plugins/email/vendor/symfony/service-contracts/composer.json b/plugins/email/vendor/symfony/service-contracts/composer.json new file mode 100644 index 0000000..f058637 --- /dev/null +++ b/plugins/email/vendor/symfony/service-contracts/composer.json @@ -0,0 +1,42 @@ +{ + "name": "symfony/service-contracts", + "type": "library", + "description": "Generic abstractions related to writing services", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\Service\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/plugins/error/.gitignore b/plugins/error/.gitignore new file mode 100755 index 0000000..5ce2fa3 --- /dev/null +++ b/plugins/error/.gitignore @@ -0,0 +1,9 @@ +# OS Generated +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db +*.swp + +# phpstorm +.idea/* diff --git a/plugins/error/CHANGELOG.md b/plugins/error/CHANGELOG.md new file mode 100755 index 0000000..eec5161 --- /dev/null +++ b/plugins/error/CHANGELOG.md @@ -0,0 +1,91 @@ +# v1.8.0 +## 09/07/2021 + +1. [](#new) + * Require **Grav 1.7.0** + * Added support for `{% throw 404 'Not Found' %}` from twig template to show the error page +1. [](#improved) + * Do not cache 404 error pages by default + +# v1.7.1 +## 10/08/2020 + +1. [](#bugfix) + * Fixed error page being cached, fixes issue with non-existing resources which later become available + +# v1.7.0 +## 07/01/2020 + +1. [](#new) + * Require Grav v1.6 +1. [](#bugfix) + * Added translated title programmatically [#40](https://github.com/getgrav/grav-plugin-error/pull/40) + +# v1.6.2 +## 05/09/2019 + +1. [](#new) + * Fixed a few issues found by phpstan + * Added `ru` and `uk` translations [#36](https://github.com/getgrav/grav-plugin-error/pull/36) + +# v1.6.1 +## 03/09/2018 + +1. [](#improved) + * Added Polish + Catalan translation + * Updated `README.md` to reference custom error pages + +# v1.6.0 +## 10/19/2016 + +1. [](#improved) + * Added Croatian translation + * Improved `autoescape: true` support +1. [](#bugfix) + * Fixed issue where template file for `error` page type is only available if page was not found + +# v1.5.1 +## 07/18/2016 + +1. [](#improved) + * Added chinese and german translations +1. [](#bugfix) + * Fixed issue with the Smartypants plugin running before Twig was processed + +# v1.5.0 +## 07/14/2015 + +1. [](#improved) + * Translate some blueprint configuration options + * Allow translating the error message + * Added french, russian, romanian, danish, italian + +# v1.4.1 +## 12/11/2015 + +1. [](#bugfix) + * Fixed CLI command for PHP 5.5 and lower + +# v1.4.0 +## 11/21/2015 + +1. [](#new) + * Implemented CLI commands for the plugin + +# v1.3.0 +## 08/25/2015 + +1. [](#improved) + * Added blueprints for Grav Admin plugin + +# v1.2.2 +## 01/06/2015 + +1. [](#new) + * Added a default `error.json.twig` file + +# v1.2.1 +## 11/30/2014 + +1. [](#new) + * ChangeLog started... diff --git a/plugins/error/LICENSE b/plugins/error/LICENSE new file mode 100755 index 0000000..484793a --- /dev/null +++ b/plugins/error/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Grav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/error/README.md b/plugins/error/README.md new file mode 100755 index 0000000..ef24726 --- /dev/null +++ b/plugins/error/README.md @@ -0,0 +1,93 @@ +# Grav Error Plugin + +![GPM Installation](assets/readme_1.png) + +`error` is a [Grav](http://github.com/getgrav/grav) Plugin and allows to redirect errors to nice output pages. + +This plugin is included in any package distributed that contains Grav. If you decide to clone Grav from GitHub you will most likely want to install this. + +# Installation + +Installing the Error plugin can be done in one of two ways. Our GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file. + +## GPM Installation (Preferred) + +The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's Terminal (also called the command line). From the root of your Grav install type: + + bin/gpm install error + +This will install the Error plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/error`. + +## Manual Installation + +To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `error`. You can find these files either on [GitHub](https://github.com/getgrav/grav-plugin-error) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/error + +>> NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav), the [Problems](https://github.com/getgrav/grav-plugin-problems) plugin, and a theme to be installed in order to operate. + +# Usage + +The `error` plugin doesn't require any configuration. The moment you install it, it is ready to use. + +Something you might want to do is to override the look and feel of the error page, and with Grav it is super easy. + +### Template + +Copy the template file [error.html.twig](templates/error.html.twig) into the `templates` folder of your custom theme and that is it. + +``` +/your/site/grav/user/themes/custom-theme/templates/error.html.twig +``` + +You can now edit the override and tweak it however you prefer. + +### Page + +Copy the page file [error.md](pages/error.md) into the `pages` folder of your user directory and that is it. + +``` +/your/site/grav/user/pages/error/error.md +``` + +You can now edit the override and tweak it however you prefer. + +# Custom error pages + +The configuration allows to specify pages different than `/error` for specific error codes. By default, the `404` error leads to the `/error` page. If you change that, make sure the page you point to has a `error` template (which means, its markdown file is `error.md` or in the page frontmatter you specify `template: error`. + +# CLI Usage +The `error` plugin comes with a CLI command that outputs the `grav.log` in a beautified way, with possibility of limiting the amount of errors displayed, as well as include the trace in the output. + +### Commands + +| `bin/plugin error log` | | +|------------------------|-----------------------------------------------------------------| +| [ --limit N \| -l N ] | The amount of errors to display. Default is 5 | +| [ --trace \| -t ] | When used, it will add the backtrace in the output of the error | + + +# Updating + +As development for the Error plugin continues, new versions may become available that add additional features and functionality, improve compatibility with newer Grav releases, and generally provide a better user experience. Updating Error is easy, and can be done through Grav's GPM system, as well as manually. + +## GPM Update (Preferred) + +The simplest way to update this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm). You can do this with this by navigating to the root directory of your Grav install using your system's Terminal (also called command line) and typing the following: + + bin/gpm update error + +This command will check your Grav install to see if your Error plugin is due for an update. If a newer release is found, you will be asked whether or not you wish to update. To continue, type `y` and hit enter. The plugin will automatically update and clear Grav's cache. + +## Manual Update + +Manually updating Error is pretty simple. Here is what you will need to do to get this done: + +* Delete the `your/site/user/plugins/error` directory. +* Download the new version of the Error plugin from either [GitHub](https://github.com/getgrav/grav-plugin-error) or [GetGrav.org](http://getgrav.org/downloads/plugins#extras). +* Unzip the zip file in `your/site/user/plugins` and rename the resulting folder to `error`. +* Clear the Grav cache. The simplest way to do this is by going to the root Grav directory in terminal and typing `bin/grav clear-cache`. + +> Note: Any changes you have made to any of the files listed under this directory will also be removed and replaced by the new set. Any files located elsewhere (for example a YAML settings file placed in `user/config/plugins`) will remain intact. diff --git a/plugins/error/assets/readme_1.png b/plugins/error/assets/readme_1.png new file mode 100755 index 0000000..930b87b Binary files /dev/null and b/plugins/error/assets/readme_1.png differ diff --git a/plugins/error/blueprints.yaml b/plugins/error/blueprints.yaml new file mode 100755 index 0000000..b642158 --- /dev/null +++ b/plugins/error/blueprints.yaml @@ -0,0 +1,36 @@ +name: Error +version: 1.8.0 +description: Displays the error page. +type: plugin +slug: error +icon: warning +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +homepage: https://github.com/getgrav/grav-plugin-error +keywords: error, plugin, required +bugs: https://github.com/getgrav/grav-plugin-error/issues +license: MIT +dependencies: + - { name: grav, version: '>=1.7.0' } + +form: + validation: strict + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + routes.404: + type: text + size: medium + label: PLUGIN_ERROR.ROUTE_404 + default: '/error' diff --git a/plugins/error/cli/LogCommand.php b/plugins/error/cli/LogCommand.php new file mode 100755 index 0000000..d29354a --- /dev/null +++ b/plugins/error/cli/LogCommand.php @@ -0,0 +1,130 @@ + 'green', + 'INFO' => 'cyan', + 'NOTICE' => 'yellow', + 'WARNING' => 'yellow', + 'ERROR' => 'red', + 'CRITICAL' => 'red', + 'ALERT' => 'red', + 'EMERGENCY' => 'magenta' + ]; + + /** + * + */ + protected function configure() + { + $this->logfile = LOG_DIR . 'grav.log'; + $this + ->setName("log") + ->setDescription("Outputs the Error Log") + ->addOption( + 'trace', + 't', + InputOption::VALUE_NONE, + 'Include the errors stack trace in the output' + ) + ->addOption( + 'limit', + 'l', + InputArgument::OPTIONAL, + 'Outputs only the last X amount of errors. Use as --limit 10 / -l 10 [default 5]', + 5 + ) + ->setHelp('The log outputs the Errors Log in Console') + ; + } + + /** + * @return int|null|void + */ + protected function serve() + { + $this->options = [ + 'trace' => $this->input->getOption('trace'), + 'limit' => $this->input->getOption('limit') + ]; + + if (!file_exists($this->logfile)) { + $this->output->writeln("\n" . "Log file not found." . "\n"); + exit; + } + + $log = file_get_contents($this->logfile); + $lines = explode("\n", $log); + + if (!is_numeric($this->options['limit'])) { + $this->options['limit'] = 5; + } + + $lines = array_slice($lines, -($this->options['limit'] + 1)); + + foreach ($lines as $line) { + $this->output->writeln($this->parseLine($line)); + } + } + + /** + * @param string $line + * + * @return null|string + */ + protected function parseLine($line) + { + $bit = explode(': ', $line); + $line1 = explode('] ', $bit[0]); + + if (!$line1[0]) { + return null; + } + + $line2 = explode(' - ', $bit[1]); + + $date = $line1[0] . ']'; + $type = str_replace('grav.', '', $line1[1]); + $color = $this->colors[$type]; + $error = $line2[0]; + $trace = implode(': ', array_slice($bit, 2)); + + $output = []; + + $output[] = ''; + $output[] = '' . $date . ''; + $output[] = sprintf(' <%s>%s ' . $error . '', $color, $type, $color); + + if ($this->options['trace']) { + $output[] = ' TRACE: '; + $output[] = ' ' . $trace; + } + + $output[] = '' . str_repeat('-', strlen($date)) . ''; + + return implode("\n", $output); + } +} + diff --git a/plugins/error/composer.json b/plugins/error/composer.json new file mode 100755 index 0000000..27ddfa9 --- /dev/null +++ b/plugins/error/composer.json @@ -0,0 +1,39 @@ +{ + "name": "getgrav/grav-plugin-error", + "type": "grav-plugin", + "description": "Error plugin for Grav CMS", + "keywords": ["error", "plugin"], + "homepage": "https://github.com/getgrav/grav-plugin-error", + "license": "MIT", + "authors": [ + { + "name": "Team Grav", + "email": "devs@getgrav.org", + "homepage": "https://getgrav.org", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/getgrav/grav-plugin-error/issues", + "irc": "https://chat.getgrav.org", + "forum": "https://getgrav.org/forum", + "docs": "https://github.com/getgrav/grav-plugin-error/blob/master/README.md" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\Console\\": "cli/" + }, + "classmap": [ + "error.php" + ] + }, + "config": { + "platform": { + "php": "7.1.3" + } + }, + "scripts": { + "test": "vendor/bin/codecept run unit", + "test-windows": "vendor\\bin\\codecept run unit" + } +} diff --git a/plugins/error/error.php b/plugins/error/error.php new file mode 100755 index 0000000..edbaaff --- /dev/null +++ b/plugins/error/error.php @@ -0,0 +1,117 @@ + [ + ['autoload', 100000], + ], + 'onPageNotFound' => [ + ['onPageNotFound', 0] + ], + 'onGetPageTemplates' => [ + ['onGetPageTemplates', 0] + ], + 'onTwigTemplatePaths' => [ + ['onTwigTemplatePaths', -10] + ], + 'onDisplayErrorPage.404'=> [ + ['onDisplayErrorPage404', -1] + ] + ]; + } + + /** + * [onPluginsInitialized:100000] Composer autoload. + * + * @return ClassLoader + */ + public function autoload(): ClassLoader + { + return require __DIR__ . '/vendor/autoload.php'; + } + + /** + * @param Event $event + */ + public function onDisplayErrorPage404(Event $event): void + { + if ($this->isAdmin()) { + return; + } + + $event['page'] = $this->getErrorPage(); + $event->stopPropagation(); + } + + /** + * Display error page if no page was found for the current route. + * + * @param Event $event + */ + public function onPageNotFound(Event $event): void + { + $event->page = $this->getErrorPage(); + $event->stopPropagation(); + } + + /** + * @return PageInterface + * @throws \Exception + */ + public function getErrorPage(): PageInterface + { + /** @var Pages $pages */ + $pages = $this->grav['pages']; + + // Try to load user error page. + $page = $pages->dispatch($this->config->get('plugins.error.routes.404', '/error'), true); + if (!$page) { + // If none provided use built in error page. + $page = new Page; + $page->init(new \SplFileInfo(__DIR__ . '/pages/error.md')); + $page->title($this->grav['language']->translate('PLUGIN_ERROR.ERROR') . ' ' . $page->header()->http_response_code); + + } + + // Login page may not have the correct Cache-Control header set, force no-store for the proxies. + $cacheControl = $page->cacheControl(); + if (!$cacheControl) { + $page->cacheControl('private, no-cache, must-revalidate'); + } + + return $page; + } + + /** + * Add page template types. + */ + public function onGetPageTemplates(Event $event): void + { + /** @var Types $types */ + $types = $event->types; + $types->register('error'); + } + + /** + * Add current directory to twig lookup paths. + */ + public function onTwigTemplatePaths(): void + { + $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; + } +} diff --git a/plugins/error/error.yaml b/plugins/error/error.yaml new file mode 100755 index 0000000..0a51d4c --- /dev/null +++ b/plugins/error/error.yaml @@ -0,0 +1,3 @@ +enabled: true +routes: + 404: '/error' diff --git a/plugins/error/hebe.json b/plugins/error/hebe.json new file mode 100755 index 0000000..3ca6ad8 --- /dev/null +++ b/plugins/error/hebe.json @@ -0,0 +1,15 @@ +{ + "project":"grav-plugin-error", + "platforms":{ + "grav":{ + "nodes":{ + "plugin":[ + { + "source":"/", + "destination":"/user/plugins/error" + } + ] + } + } + } +} diff --git a/plugins/error/languages.yaml b/plugins/error/languages.yaml new file mode 100755 index 0000000..650e617 --- /dev/null +++ b/plugins/error/languages.yaml @@ -0,0 +1,55 @@ +en: + PLUGIN_ERROR: + ERROR: "Error" + ERROR_MESSAGE: "Woops. Looks like this page doesn't exist." + ROUTE_404: "404 Route" +de: + PLUGIN_ERROR: + ERROR: "Fehler" + ERROR_MESSAGE: "Uuups. Sieht aus als ob diese Seite nicht existiert." +hr: + PLUGIN_ERROR: + ERROR: "Greška" + ERROR_MESSAGE: "Uups. Izgleda da ova stranica ne postoji." +ro: + PLUGIN_ERROR: + ERROR: "Eroare" + ERROR_MESSAGE: "Ooops. Se pare că pagina nu există." +fr: + PLUGIN_ERROR: + ERROR: "Erreur" + ERROR_MESSAGE: "Oups. Il semble que cette page n’existe pas." +it: + PLUGIN_ERROR: + ERROR: "Errore" + ERROR_MESSAGE: "Ooops. A quanto pare, questa pagina non esiste." +ru: + PLUGIN_ERROR: + ERROR: "Ошибка" + ERROR_MESSAGE: "Упс. Похоже, этой страницы не существует." + ROUTE_404: "Маршрут 404" +uk: + PLUGIN_ERROR: + ERROR: "Помилка" + ERROR_MESSAGE: "Упс. Схоже, цієї сторінки не існує." + ROUTE_404: "Маршрут 404" +da: + PLUGIN_ERROR: + ERROR: "Fejl" + ERROR_MESSAGE: "Ups. Det ser ud til at siden ikke eksisterer." +zh: + PLUGIN_ERROR: + ERROR: "错误" + ERROR_MESSAGE: "呃,似乎这个页面不存在。" +cs: + PLUGIN_ERROR: + ERROR: "Chyba" + ERROR_MESSAGE: "A jéje. Vypadá to, že hledaná stránka tu není." +pl: + PLUGIN_ERROR: + ERROR: "Błąd" + ERROR_MESSAGE: "Ups. Wygląda na to, że ta strona nie istnieje." +ca: + PLUGIN_ERROR: + ERROR: "Error" + ERROR_MESSAGE: "Ups. Sembla que aquesta pàgina no existeix." diff --git a/plugins/error/templates/error.html.twig b/plugins/error/templates/error.html.twig new file mode 100755 index 0000000..420702b --- /dev/null +++ b/plugins/error/templates/error.html.twig @@ -0,0 +1,3 @@ +

    {{ 'PLUGIN_ERROR.ERROR'|t }} {{ header.http_response_code }}

    + +

    {{ page.content|raw }}

    diff --git a/plugins/error/templates/error.json.twig b/plugins/error/templates/error.json.twig new file mode 100755 index 0000000..27472f1 --- /dev/null +++ b/plugins/error/templates/error.json.twig @@ -0,0 +1 @@ +{{ page.content|json_encode()|raw }} \ No newline at end of file diff --git a/plugins/error/vendor/autoload.php b/plugins/error/vendor/autoload.php new file mode 100755 index 0000000..b30c4e0 --- /dev/null +++ b/plugins/error/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath.'\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/plugins/error/vendor/composer/LICENSE b/plugins/error/vendor/composer/LICENSE new file mode 100755 index 0000000..f27399a --- /dev/null +++ b/plugins/error/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/error/vendor/composer/autoload_classmap.php b/plugins/error/vendor/composer/autoload_classmap.php new file mode 100755 index 0000000..71c4666 --- /dev/null +++ b/plugins/error/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $baseDir . '/error.php', +); diff --git a/plugins/error/vendor/composer/autoload_namespaces.php b/plugins/error/vendor/composer/autoload_namespaces.php new file mode 100755 index 0000000..b7fc012 --- /dev/null +++ b/plugins/error/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/cli'), +); diff --git a/plugins/error/vendor/composer/autoload_real.php b/plugins/error/vendor/composer/autoload_real.php new file mode 100755 index 0000000..ee6a4c0 --- /dev/null +++ b/plugins/error/vendor/composer/autoload_real.php @@ -0,0 +1,52 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit4e2f762c713c4d4aae8969c74f5623a3::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/plugins/error/vendor/composer/autoload_static.php b/plugins/error/vendor/composer/autoload_static.php new file mode 100755 index 0000000..11ca02a --- /dev/null +++ b/plugins/error/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'Grav\\Plugin\\Console\\' => 20, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Grav\\Plugin\\Console\\' => + array ( + 0 => __DIR__ . '/../..' . '/cli', + ), + ); + + public static $classMap = array ( + 'Grav\\Plugin\\ErrorPlugin' => __DIR__ . '/../..' . '/error.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit4e2f762c713c4d4aae8969c74f5623a3::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit4e2f762c713c4d4aae8969c74f5623a3::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit4e2f762c713c4d4aae8969c74f5623a3::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/plugins/error/vendor/composer/installed.json b/plugins/error/vendor/composer/installed.json new file mode 100755 index 0000000..fe51488 --- /dev/null +++ b/plugins/error/vendor/composer/installed.json @@ -0,0 +1 @@ +[] diff --git a/plugins/flex-objects/.eslintrc b/plugins/flex-objects/.eslintrc new file mode 100644 index 0000000..e62c1d5 --- /dev/null +++ b/plugins/flex-objects/.eslintrc @@ -0,0 +1,170 @@ +{ + "root": true, + "env": { + "browser": true, + "node": true, + "es6": true + }, + + "parser": "@babel/eslint-parser", + + "parserOptions": { + "ecmaVersion": 7, + "sourceType": "module", + "requireConfigFile": false + }, + + "rules": { + "accessor-pairs": 2, + "array-bracket-spacing": 0, + "block-scoped-var": 0, + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": 0, + "comma-dangle": [2, "never"], + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "complexity": 0, + "computed-property-spacing": 0, + "consistent-return": 0, + "consistent-this": 0, + "constructor-super": 2, + "curly": [2, "multi-line"], + "default-case": 0, + "dot-location": [2, "property"], + "dot-notation": 0, + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + "func-names": 0, + "func-style": 0, + "generator-star-spacing": [2, { "before": true, "after": true }], + "guard-for-in": 0, + "handle-callback-err": [2, "^(err|error)$" ], + "indent": [2, 4, { "SwitchCase": 1 }], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + "linebreak-style": 0, + "lines-around-comment": 0, + "max-nested-callbacks": 0, + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "newline-after-var": 0, + "no-alert": 0, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 0, + "no-cond-assign": 2, + "no-console": 0, + "no-constant-condition": 0, + "no-continue": 0, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 0, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-else-return": 0, + "no-empty": 0, + "no-empty-character-class": 2, + "no-eq-null": 0, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 0, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inline-comments": 0, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-lonely-if": 0, + "no-loop-func": 0, + "no-mixed-requires": 0, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 0, + "no-new": 2, + "no-new-func": 0, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 0, + "no-path-concat": 0, + "no-process-env": 0, + "no-process-exit": 0, + "no-proto": 0, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 0, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 0, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-sync": 0, + "no-ternary": 0, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unexpected-multiline": 2, + "no-unneeded-ternary": 2, + "no-unreachable": 2, + "no-unused-expressions": 0, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-use-before-define": 0, + "no-var": 0, + "no-void": 0, + "no-warning-comments": 0, + "no-with": 2, + "object-curly-spacing": 0, + "object-shorthand": 0, + "one-var": [2, { "initialized": "never" }], + "operator-assignment": 0, + "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], + "padded-blocks": 0, + "prefer-const": 0, + "quote-props": 0, + "quotes": [2, "single", "avoid-escape"], + "radix": 2, + "semi": [2, "always"], + "semi-spacing": 0, + "sort-vars": 0, + "keyword-spacing": [2, {"after": true, "overrides": {"throw": { "after": true}, "return": { "before": true }}}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!"] }], + "strict": 0, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": [2, "any"], + "wrap-regex": 0, + "yoda": [2, "never"] + } +} diff --git a/plugins/flex-objects/.gitignore b/plugins/flex-objects/.gitignore new file mode 100644 index 0000000..0ac92e1 --- /dev/null +++ b/plugins/flex-objects/.gitignore @@ -0,0 +1,3 @@ +.idea +.DS_Store +node_modules diff --git a/plugins/flex-objects/CHANGELOG.md b/plugins/flex-objects/CHANGELOG.md new file mode 100644 index 0000000..f18cefa --- /dev/null +++ b/plugins/flex-objects/CHANGELOG.md @@ -0,0 +1,550 @@ +# v1.3.5 +## 05/09/2023 + +1. [](#improved) + * Various deprecation fixes for PHP 8.2+ + +# v1.3.4 +## 02/19/2023 + +1. [](#improved) + * Support saving via admin in current language if not translated + +# v1.3.3 +## 01/04/2023 + +1. [](#improved) + * Save `post-save` action to session + * Set default `post-save` action to `edit` for create and edit + +# v1.3.2 +## 12/02/2022 + +1. [](#improved) + * Various translation enhancements +1. [](#bugfix) + * Fixed frontend editing objects with urlencoded ids + +# v1.3.1 +## 09/08/2022 + +1. [](#bugfix) + * Fixed `covnertUrls` action + +# v1.3.0 +## 06/14/2022 + +1. [](#new) + * Added user object to `onFlexTask.*` and `onFlexAction.*` events + * Added tasks `MediaUploadMeta` and `MediaReorder` to support remote media fields + * Added support to remove media defined in a field +2. [](#improved) + * Refactored admin controller tasks and actions + * Added image preview support for 3rd party editors +1. [](#bugfix) + * Fixed broken error responses in object media tasks + +# v1.2.0 +## 03/28/2022 + +1. [](#new) + * Require **Grav 1.7.32** and **Form 6.0.0** +2. [](#improved) + * Improved flex router event to include directory +3. [](#bugfix) + * Fixed caching issues in dynamic flex forms + * Fixed flex content in unauthorized module causing the whole page to become unauthorized + +# v1.1.9 +## 03/14/2022 + +1. [](#new) + * Added support for flex router to return a response instead of a page + +# v1.1.8 +## 01/28/2022 + +1. [](#new) + * Require **Grav 1.7.29** +3. [](#improved) + * Made path handling unicode-safe, use new `Utils::basename()` and `Utils::pathinfo()` everywhere + +# v1.1.7 +## 01/03/2022 + +1. [](#new) + * Allow intercepting object `create`, `update` and `delete` tasks by using `FlexTaskEvent` event +2. [](#improved) + * Added optional `$scope` parameter to `ObjectController::checkAuthorization()` +3. [](#bugfix) + * Fixed continue task with `PageInterface` types + +# v1.1.6 +## 11/29/2021 + +1. [](#bugfix) + * Fixed regression `Call to a member function getRoute() on null` when using CLI [#151](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/151) + +# v1.1.5 +## 11/24/2021 + +1. [](#new) + * Added method `ObjectController::checkAuthorizations()` to check if one of the actions is true +2. [](#bugfix) + * Fixed regression when calling flex router with a path + +# v1.1.4 +## 11/16/2021 + +1. [](#new) + * Require **Grav 1.7.25** +1. [](#improved) + * Changed flex router not to trigger `onPageNotFound` event + * Changed flex router to be called also with empty path + * If ACL check for the object fails, display unauthorized page instead of 404 +1. [](#bugfix) + * Fixed unescaped messages in JSON responses + * Fixed `Call to a member function getName() on null` when using file field [#149](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/149) + +# v1.1.3 +## 10/26/2021 + +1. [](#improved) + * Updated JS dependencies to latest + * Optimized import of certain JS dependencies + * Dev: Moved away from deprecated UglifyJsPlugin in favor of TerserPlugin + * Use active form from the Form plugin to get page metadata + * Added page header `flex.access.override: true`, which allows flex to replace page `access` when user is allowed to perform action in flex +1. [](#bugfix) + * Fixed flex object page access for super users when permission was denied + +# v1.1.2 +## 09/14/2021 + +1. [](#new) + * Require **Grav 1.7.21**, optionally **Error 1.8.0**, **Login 3.5.2** and **Form 5.1.1** + * Added file upload/delete support to frontend forms + * Support proper error, login and unauthorized pages if all requirements are met + * Added page header `flex.router: [ROUTER]` which triggers `flex.router.[ROUTER]` event for child routes of the page + * Added `flex.[type].task.create.after`, `flex.[type].task.update.after` and `flex.[type].task.delete.after` events for frontend + +# v1.1.1 +## 09/01/2021 + +1. [](#bugfix) + * Fixed XSS in page admin + * Fixed check for bad folder name, prevent bad characters + +# v1.1.0 +## 08/31/2021 + +1. [](#new) + * Require **Grav 1.7.19** and **Form 5.1.0** + * Added basic frontend editing support + * Added `onBeforeFlexFormInitialize` event to help to initialize the frontend form +1. [](#bugfix) + * Fixed error in admin when field validation fails + +# v1.0.16 +## 07/19/2021 + +1. [](#new) + * Added basic new modal support for all flex types +1. [](#bugfix) + * Fixed authorization check for user configuration + +# v1.0.15 +## 06/16/2021 + +1. [](#improved) + * Better checks against missing Flex Type inside tasks + * Better authorization checks, falls back to directory level authorization checks if objects do not support authorization +1. [](#bugfix) + * Fixed missing handling of child_type in Flex Pages [getgrav/grav-plugin-admin#2087](https://github.com/getgrav/grav-plugin-admin/issues/2087) + * Added support for multiple `Exports` in a dropdown + * Admin is no longer a dependency of Flex Objects [#130](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/130) + * Fixed authorization checks during page creation for users who have limited access to some pages [getgrav/grav#3382](https://github.com/getgrav/grav/issues/3382) + * Fixed permission check when moving a page [getgrav/grav#3382](https://github.com/getgrav/grav/issues/3382) + +# v1.0.14 +## 06/07/2021 + +1. [](#improved) + * Added enhanced copy modal from Pages list [getgrav/grav-plugin-admin#2139](https://github.com/getgrav/grav-plugin-admin/issues/2139) + +# v1.0.13 +## 06/03/2021 + +1. [](#bugfix) + * Fixed expert mode for Flex Pages + +# v1.0.12 +## 06/02/2021 + +1. [](#bugfix) + * Fixed logic to get form blueprints and object, prevents events from being fired twice + * Fixed breadcrumb item in Pages list not translating HTML entities [#127](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/127) + +# v1.0.11 +## 05/24/2021 + +1. [](#improved) + * Allow file uploads to send data such as `data[media_order]` + +# v1.0.10 +## 05/19/2021 + +1. [](#bugfix) + * Fixed `Add Folder` not updating the page list until cache is cleared + * Fixed broken error message translations + +# v1.0.9 +## 04/29/2021 + +1. [](#bugfix) + * Fixed fatal error when copying a page in admin if no modal is being shown [getgrav/grav#3335](https://github.com/getgrav/grav/issues/3335) + +# v1.0.8 +## 04/23/2021 + +1. [](#new) + * Require **Admin 1.10.13** + * Require **Form Plugin 5.0.2** +1. [](#improved) + * Added a few missing translations + * Utilize new Admin detector to prevent Save actions that triggers unsaved notice on unload [getgrav/grav-plugin-admin#2125](https://github.com/getgrav/grav-plugin-admin/issues/2125) + * Improved copying page by adding a modal for new page title and folder name + +# v1.0.7 +## 04/06/2021 + +1. [](#new) + * Require **Grav 1.7.10** + * Added deny option support to `filepicker` field [#119](https://github.com/trilbymedia/grav-plugin-flex-objects/pull/119) +1. [](#bugfix) + * Prevent expert editing mode from anyone else than super users [grav-plugin-admin#2094](https://github.com/getgrav/grav-plugin-admin/issues/2094) + * Fixed not being able to add new folder [grav#3293](https://github.com/getgrav/grav/issues/3293) + * Fixed Flex directories defined only in theme not showing up [grav#3292](https://github.com/getgrav/grav/issues/3292) + +# v1.0.6 +## 03/30/2021 + +1. [](#bugfix) + * Fixed automatic git-sync in admin save and delete [#120](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/120) + * Prevent Add Page / Add Module modals from closing if clicking on the outside overlay [grav-plugin-admin#2089](https://github.com/getgrav/grav-plugin-admin/issues/2089) + +# v1.0.5 +## 03/19/2021 + +1. [](#new) + * Require **Grav 1.7.9** + * Require **Form Plugin 5.0.1** +1. [](#improved) + * Catch JSON decoding issues in controllers +1. [](#bugfix) + * Fixed broken media upload/picker fields with `@self/path` notations [grav#3275](https://github.com/getgrav/grav/issues/3275) + * Fixed `filepicker` field not including newly uploaded and excluding newly deleted files before saving the object + * Fixed `Flex Page` CRUD ACL when creating a new page [#115](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/115) + * Bumped dependencies versions [#116](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/116) + * Fixed clicking `move` button on some pages resulting in endless loading spinner [grav-plugin-admin#2095](https://github.com/getgrav/grav-plugin-admin/issues/2095) + +# v1.0.4 +## 03/17/2021 + +1. [](#improved) + * Added id attributes for buttons to help on acceptance testing +1. [](#bugfix) + * Fixed fatal error in `/admin/flex-objects` [#114](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/114) + * Fixed `onAdminSave` original page having empty header [grav#3259](https://github.com/getgrav/grav/issues/3259) + * Fixed flash issues on uploading files into a new page + +# v1.0.3 +## 02/17/2021 + +1. [](#improved) + * List field: added new `placement` property to decide whether to add new items at the top, bottom or based on the *position* of the clicked button [#105](https://github.com/trilbymedia/grav-plugin-flex-objects/pull/105) + * Added default styling for Flex-Objects Admin list view +1. [](#bugfix) + * Fixed fatal error if configuration is missing directories [#107](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/107) + * Fixed case-sensitive `accept` in `filepicker` field + * Fixed pages admin being accessible without read/write permissions [grav-plugin-admin#2053](https://github.com/getgrav/grav-plugin-admin/issues/2053) + * Fixed missing event `onAdminCreatePageFrontmatter` when creating a new page [grav-plugin-auto-date#8](https://github.com/getgrav/grav-plugin-auto-date/issues/8) + * Fixed missing event `onAdminAfterDelMedia` when deleting a file from a page + * Fixed filepicker support for old `theme@:/` and `page@:/` notations [#109](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/109) + * Fixed adding the same new page twice remembering content from the last try + * Fixed saving a new page with invalid data makes blueprint fields disappear [grav-plugin-admin#2068](https://github.com/getgrav/grav-plugin-admin/issues/2068) + +# v1.0.2 +## 02/01/2021 + +1. [](#new) + * Require **Grav 1.7.4** +1. [](#bugfix) + * Fixed saving page in expert mode [grav#3174](https://github.com/getgrav/grav/issues/3174) + +# v1.0.1 +## 01/20/2021 + +1. [](#bugfix) + * Fixed 404 when trying to edit a page with accented characters [grav-plugin-admin#2026](https://github.com/getgrav/grav-plugin-admin/issues/2026) + +# v1.0.0 +## 01/19/2021 + +1. [](#new) + * Added `$grav['flex_objects']->getAdminController()` method +1. [](#improved) + * Added support for relative paths in `getLevelListing` action +1. [](#bugfix) + * Fixed admin not working with types that do not implement `FlexAuthorizeInterface` + * Fixed bad redirect when creating new flex object and choosing to create another return to the list + * Fixed bad redirect when changing parent of new page and saving [grav-plugin-admin#2014](https://github.com/getgrav/grav-plugin-admin/issues/2014) + * Fixed page forms being empty if multi-language is enabled, but there's just one language [grav#3147](https://github.com/getgrav/grav/issues/3147) + * Fixed copying a page within a parent with no create permission [grav-plugin-admin#2002](https://github.com/getgrav/grav-plugin-admin/issues/2002) + +# v1.0.0-rc.20 +## 12/15/2020 + +1. [](#improved) + * Default cookies usage to SameSite Lax [grav-plugin-admin#1998](https://github.com/getgrav/grav-plugin-admin/issues/1998) + * Fixed typo [#89](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/89) + +# v1.0.0-rc.19 +## 12/02/2020 + +1. [](#improved) + * Just keeping sync with Grav rc.19 + +# v1.0.0-rc.18 +## 12/02/2020 + +1. [](#new) + * Require **PHP 7.3.6** +1. [](#improved) + * Improved frontend templates + * Improve blueprint structure + * Hooked up Duplicate and Move from within Pages list [#81](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/81) + * Respect CRUD ACL actions for items shortcuts in pages list [#82](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/82) + * Refresh object on controllers to make sure it is up to date +1. [](#bugfix) + * Fixed fatal error in admin if list view hasn't been defined + * Fixed fatal error in admin if directory throws exception + * Fixed attempts to add an existing page + * Fixed form loosing its form state if saving fails when using `ObjectController` + * Fixed missing context when rendering collection in frontend + * Fixed Flex Admin activating on too old Admin plugin versions + +# v1.0.0-rc.17 +## 10/07/2020 + +1. [](#bugfix) + * Fixed media uploads for objects which do not implement `FlexAuthorizeInterface` + * Fixed file picker field not recognizing `folder: @self` variants + +# v1.0.0-rc.16 +## 09/01/2020 + +1. [](#improved) + * Simplified `Flex Pages` admin not to differentiate between default language file extensions [#47](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/47) +1. [](#bugfix) + * Fixed extra space in Flex admin pages + * Fixed folder creation with parent other than root [#66](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/66) + * Fixed task redirects in sub-folder multi-site environments + * Fixed typo in default permissions (should have been `admin.flex-objects`) [grav#2915](https://github.com/getgrav/grav/issues/2915) + +# v1.0.0-rc.15 +## 07/22/2020 + +1. [](#new) + * Released with no changes to keep sync with Grav + Admin + +# v1.0.0-rc.14 +## 07/09/2020 + +1. [](#new) + * Released with no changes to keep sync with Grav + Admin + +# v1.0.0-rc.13 +## 07/01/2020 + +1. [](#bugfix) + * Fixed bad link in directory listing template + * Fixed admin save task displaying error message about non-existing data type + * Fixed `pagemedia` field not uploading/deleting files right away + * Fixed `Flex Pages` add, copy and move buttons appearing in edit view when no permissions + * Fixed `Flex Pages` permission issues + * Fixed some admin redirect issues + +# v1.0.0-rc.12 +## 06/08/2020 + +1. [](#new) + * Code updates to match Grav 1.7.0-rc.12 +1. [](#improved) + * Changed class `admin-pages` to `admin-{{ target }}` [#59](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/59) + +# v1.0.0-rc.11 +## 05/14/2020 + +1. [](#new) + * Added integration with Admin's new preset events to style the CSS +1. [](#improved) + * JS Maitenance +1. [](#bugfix) + * Fixed `Accounts` Configuration tab + +# v1.0.0-rc.10 +## 04/27/2020 + +1. [](#bugfix) + * Fixed custom actions not working + * Fixed custom folder in `mediapicker` field not working + * Fixed export title when not using CVS [#51](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/51) + * Fixed preview in Page list view [admin#1845](https://github.com/getgrav/grav-plugin-admin/issues/1845) + * Fixed `404 Not Found` error after saving a new object + +# v1.0.0-rc.9 +## 03/20/2020 + +1. [](#bugfix) + * Fixed issue with touch devices and scrollbars hidden, preventing native scrolling to work [admin#1857](https://github.com/getgrav/grav-plugin-admin/issues/1857) [#1858](https://github.com/getgrav/grav-plugin-admin/issues/1858) + + +# v1.0.0-rc.8 +## 03/19/2020 + +1. [](#new) + * Added a basic **Convert Data** CLI Command. Works with `Yaml` <-> `Json` +1. [](#bugfix) + * Fixed jump of the page when applying filters [grav-admin#1830](https://github.com/getgrav/grav-plugin-admin/issues/1830) + * Fixed form resetting when validation fails [grav#2764](https://github.com/getgrav/grav/issues/2764) + +# v1.0.0-rc.7 +## 03/05/2020 + +1. [](#new) + * Added option to change perPage amount of items in Flex List. 'All' also available by only at runtime. +1. [](#improved) + * Page filters now obey admin hide type settings +1. [](#bugfix) + * Fixed fatal error if there is missing blueprint [grav#2834](https://github.com/getgrav/grav/issues/2834) + * Fixed redirect when moving a page [grav#2829](https://github.com/getgrav/grav/issues/2829) + * Fixed no default access set when creating new user from admin [#31](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/31) + * Flex Pages: Fixed page visibility issues when creating a new page [grav#2823](https://github.com/getgrav/grav/issues/2823) + * Flex Pages: Fixed translated page having non-translated status with `system.languages.include_default_lang_file_extension: false` + * Flex Pages: Fixed preview on home page + +# v1.0.0-rc.6 +## 02/11/2020 + +1. [](#new) + * Pass phpstan level 1 tests + * Removed legacy classes for pages, cleanup deprecated Flex types +1. [](#bugfix) + * Fixed call to `grav.flex_objects.getObject()` causing fatal error + * Minor bug fixes + +# v1.0.0-rc.5 +## 02/03/2020 + +1. [](#new) + * No changes, just keeping things in sync with Grav RC version + +# v1.0.0-rc.4 +## 02/03/2020 + +1. [](#new) + * Added support for arbitrary admin menu route for editing a flex type + * Added support for new improved ACL + * Added support for custom layouts by adding `/:layout_name` in url + * Added support for Flex Directory specific Configuration + * Added support for action aliases (`/accounts/configure` instead of `/accounts/users/:configre`) + * Added Flex type `Configuration` + * Enabled `Pages`, `Accounts` and `User Groups` by default + * Stop using deprecated `onAdminRegisterPermissions` event + * Renamed directory `grav-pages` to `pages` + * Renamed directory `grav-accounts` to `user-accounts` + * Renamed directory `grav-user-groups` to `user-groups` +1. [](#improved) + * Flex caching settings were moved into Grav core + * Flex Objects plugin now better integrates to Grav core +1. [](#bugfix) + * Fixed empty directory entries in plugin configuration + * Fixed plugin configuration displaying directories outside of the plugin + * Fixed broken blueprints if there's folder with the name of the blueprint file + * Fixed visible save button when in 404 page + * Fixed missing save location when file does not exist + * Fixed multiple ACL related issues (no access, bad links, information leaks) + * Fixed Admin Panel Page list buttons not appearing in Flex Pages + +# v1.0.0-rc.3 +## 01/02/2020 + +1. [](#new) + * Added root page support for `Flex Pages` +1. [](#bugfix) + * Fixed after save: Edit + * Fixed JS failing on initial filters setup due to no fallback implemented [#2724](https://github.com/getgrav/grav/issues/2724) + +# v1.0.0-rc.2 +## 12/04/2019 + +1. [](#new) + * Admin: Added support for editing `User Groups` + * Admin: `Flex Pages` now support **searching** and **filtering** +1. [](#bugfix) + * Hide hidden/system types (pages, accounts, user groups) from Flex Objects page type [#38](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/38) + +# v1.0.0-rc.1 +## 11/06/2019 + +1. [](#new) + * Added directory configuration option for custom admin templates + * Added `Flex Accounts (Admin)` type to administer user accounts in Flex independently from Grav system setting + * Added `Flex Pages (Admin)` type to administer pages in Flex independently from Grav system setting + * Added blueprint option to hide directory from Flex Objects types page in frontend + * Deprecated all `Flex Page` classes and traits in favor of the new classes in Grav core + * Moved flex object/collection templates to `templates/flex/{TYPE}` which is easier to remember + * Admin: Added support customizable preview and export +1. [](#improved) + * Admin: Allow custom title template when editing object + * Translations: rename MODULAR to MODULE everywhere +1. [](#bugfix) + * Flex Pages: Fixed default language not being translated in both `translatedLanguages()` and `untranslatedLanguages()` results + * Flex Pages: Language interface compatibility fixes + * Flex Pages: Fixed frontend issues with plugin events [#5](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/5) + * Flex Pages: Fixed `filePathClean()` and `filePathClean()` not returning file for folder + * Flex Pages: Fixed multiple multi-language related issues in admin [#10](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/10) + * Flex Pages: Fixed raw edit mode + * File upload is broken for nested fields [#34](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/34) + +# v1.0.0-beta.10 +## 10/03/2019 + +1. [](#bugfix) + * Flex Pages: Fixed moving visible page in admin causing ordering issues [#6](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/6) + * Flex Pages List: Fixed issue where auto-hiding scrollbars in macOS would throw off the dropdown position [#20](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/20) + * Flex Pages: Fixed prev/next page missing pages if pagination was turned on in page header + +# v1.0.0-beta.9 +## 09/26/2019 + +1. [](#improved) + * Show/hide dropdown menu as needed when scrolling the page columns container left and right +1. [](#bugfix) + * PHP 7.1: Fixed error when activating `Flex Pages` in Plugin parameters [#13](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/13) + * Flex Pages: Fixed page template cannot be changed [#4](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/4) + * Flex Pages: Fixed new pages being created with wrong template [#22](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/22) + * Flex Pages: Fixed `Preview` not working [#17](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/17) + * Fixed error caused by automatic path selection from cookie when destination not available [#23](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/23) + * Fixed breadcrumb issue in Flex Pages List [#19](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/19) + * Flex Pages: Fixed unable to change page template [#4](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/4) + * Fixed `Error 404` when adding new contact [#14](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/14) + * Flex Pages: Non-visible items appear in Nav menu [#24](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/24) + * Disabling plugin breaks saving plugin configuration [#11](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/11) + +# v1.0.0-beta.8 +## 09/19/2019 + +1. [](#new) + * Initial public release (all previous versions were in a private repo) diff --git a/plugins/flex-objects/LICENSE b/plugins/flex-objects/LICENSE new file mode 100644 index 0000000..d151da3 --- /dev/null +++ b/plugins/flex-objects/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Trilby Media, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/flex-objects/README.md b/plugins/flex-objects/README.md new file mode 100644 index 0000000..976355f --- /dev/null +++ b/plugins/flex-objects/README.md @@ -0,0 +1,289 @@ +# Flex Objects Plugin + +## About + +The **Flex Objects** Plugin is for [Grav CMS](https://github.com/getgrav/grav). Flex objects is a powerful new plugin that allows you to build custom collections of objects, which can modified by CRUD operations via the admin plugin to easily manage large sets of data that don't make sense as simple YAML configuration files, or Grav pages. These objects are defined by blueprints written in YAML and they are rendered by a set of twig files. Additionally both objects and collections can be customized by PHP classes, which allows you to define complex behaviors and relationships between the objects. + +![](assets/flex-objects-list.png) + +![](assets/flex-objects-edit.png) + +![](assets/flex-objects-options.png) + + +## System Requirements + +Plugin requires **Grav** v1.7.25 or later version in order to run. Additionally you need **Form Plugin** v5.1.0 and optionally **Admin Plugin** v1.10.25 or later version. + +## Installation + +Typically a plugin should be installed via [GPM](http://learn.getgrav.org/advanced/grav-gpm) (Grav Package Manager): + +``` +$ bin/gpm install flex-objects +``` + +Alternatively it can be installed via the [Admin Plugin](http://learn.getgrav.org/admin-panel/plugins) + +## Sample Data + +Once installed you can either create entries manually, or you can copy the sample data set: + +```shell +$ mkdir -p user/data/flex-objects +$ cp user/plugins/flex-objects/data/flex-objects/contacts.json user/data/flex-objects/contacts.json +``` + +## Configuration + +This plugin works out of the box, but provides several fields that make modifying and extending this plugin easier: + +```yaml +enabled: true + +built_in_css: true +extra_admin_twig_path: 'theme://admin/templates' +admin_list: + per_page: 15 + order: + by: updated_timestamp + dir: desc + +directories: + - 'blueprints://flex-objects/contacts.yaml' + - 'blueprints://flex-objects/pages.yaml' + - 'blueprints://flex-objects/user-accounts.yaml' + - 'blueprints://flex-objects/user-groups.yaml' +``` + +Simply edit the **Flex Objects** plugin options in the Admin plugin, or copy the `flex-objects.yaml` default file to your `user/config/plugins/` folder and edit the values there. Read below for more help on what these fields do and how they can help you modify the plugin. + +Most interesting configuration option is `directories`, which contains list or blueprint files which will define the flex types. + +## Displaying + +![](assets/flex-objects-site.png) + +just create a page called `flex-objects.md` or set the template of your existing page to `template: flex-objects`. This will use the `flex-objects.html.twig` file provided by the plugin. + + +```twig +--- +title: Directory +flex: + directory: contacts +--- + +# Directory Example +``` + +If you do not specify `flex.directory` name in the page header, the page will list all directories instead of displaying entries from a single directory. + +![](assets/flex-objects-directory.png) + +# Modifications + +This plugin is configured with a sample contacts directory with a few sample fields: + +* published +* first_name +* last_name +* email +* website +* tags + +These are probably not the exact fields you might want, so you will probably want to change them. This is pretty simple to do with Flex Objects, you just need to change the **Blueprints** and the **Twig Templates**. This can be achieved simply enough by copying some current files and modifying them. + +Let's assume you simply want to add a new "Phone Number" field to the existing Data and remove the "Tags". These are the steps you would need to perform: + +1. Copy the `blueprints/flex-objects/contacts.yaml` Blueprint file to another location, let's say `user/blueprints/flex-objects/`. The file can really be stored anywhere, but if you are using admin, it is best to keep the blueprint file where admin can automatically find it. + +!!! **NOTE:** If you want to put the blueprints to `user/themes/yourtheme/blueprints`, you need to use the new blueprint folder structure from Grav 1.7. See [Plugin/Theme Blueprints](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide#plugin-theme-blueprints-blueprints-yaml). + +2. Edit the `user/blueprints/flex-objects/contacts.yaml` like so: + + ```yaml + title: Contacts + description: Simple contact directory with tags. + type: flex-objects + + config: + admin: + list: + title: name + fields: + published: + field: + type: toggle + label: Publ + width: 8 + last_name: + link: edit + first_name: + link: edit + email: + phone: + data: + storage: + class: 'Grav\Framework\Flex\Storage\SimpleStorage' + options: + formatter: + class: 'Grav\Framework\File\Formatter\JsonFormatter' + folder: user-data://flex-objects/contacts.json + + form: + validation: loose + + fields: + published: + type: toggle + label: Published + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + required: true + + last_name: + type: text + label: Last Name + validate: + required: true + + first_name: + type: text + label: First Name + + email: + type: email + label: Email Address + validate: + required: true + + website: + type: url + label: Website URL + + phone: + type: text + label: Phone Number + ``` + + See how we replaced `tags:` with `phone:` in the `config.admin.list.fields` section at the top. Also, notice how we removed the `tags:` Blueprint field definition, and added a simple text field for `phone:`. If you have questions about available form fields, [check out the extensive documentation](https://learn.getgrav.org/forms/blueprints/fields-available) on the subject. + +3. We need to copy the frontend Twig file and modify it to add the new "Phone" field. By default your theme already has its `templates`, so we can take advantage of it 2. We'll simply copy the `user/plugins/flex-objects/templates/flex/contacts/object/default.html.twig` file to `user/themes/quark/templates/flex/contacts/object/default.html.twig`. Notice, there is no reference to `admin/` here, this is site template, not an admin one. We are also assuming you are using `Quark` theme, so you may have to change this to reference the theme you are using. + +4. Edit the `default.html.twig` file you just copied so it has these modifications: + + ```twig +
    + {% if object.website %} + {{ object.last_name }}, {{ object.first_name }} + {% else %} + {{ object.last_name }}, {{ object.first_name }} + {% endif %} + {% if object.email %} +

    + {% endif %} + {% if object.phone %} +

    {{ object.phone }}

    + {% endif %} +
    + ``` + + Notice, we removed the `entry-extra` DIV, and added a new `if` block with the Twig code to display the phone number if set. + +5. We also need to tweak the JavaScript initialization which provides which hooks up certain classes to the search. To do this we need to copy the `user/plugins/flex-objects/templates/flex/contacts/collection/default.html.twig` file to `user/themes/quark/templates/flex/contacts/collection/default.html.twig`. Notice this is the `collection` template this time, not the `object` template as we copied before. + + Edit this file and replace the `` tag at the bottom with this code: + + ```html + + ``` + +# File Upload + +To upload files you can use the `file` form field. [The standard features apply](https://learn.getgrav.org/forms/blueprints/how-to-add-file-upload), and you can simply edit your custom blueprint with a field definition similar to: + +``` + item_image: + type: file + label: Item Image + random_name: true + destination: 'user/data/flex-objects/files' + multiple: true +``` + +> In order to fully take advantage of image uploads, you should always be using `FolderStorage`, meaning that the objects get saved to individual folders together with the images. Other storage layers may or may not support media. + +# Advanced + +You can radically alter the structure of the `contacts.json` data file by making major edits to the `contacts.yaml` blueprint file. However, it's best to start with an empty `contacts.json` if you are making wholesale changes or you will have data conflicts. Best to create your blueprint first. Reloading a **New Entry** until the form looks correct, then try saving, and check to make sure the stored `user/data/flex-objects/contacts.json` file looks correct. + +Then you will need to make more widespread changes to the site Twig templates. You might need to adjust the number of columns and the field names. You will also need to pay attention to the JavaScript initialization in each template. + +# Features + +Here are the main benefits of using Flex objects: + +* CRUD is automatically handled for you by Flex Objects plugin +* Objects can be stored using many different strategies, including single file, file per object or folder per object; using yaml, json etc. +* Flex types can be easily extended by custom PHP collection and object classes +* Both Flex objects and collections know how to render themselves: `echo $object->render($layout, $context)` or `{% render object layout: layout with context %}` +* You can easily create custom layouts for your objects and collections to be used in different pages +* Both Flex objects and collections support serialization and `json_encode()` +* Flex objects support Grav `Medium` objects with few lines of code +* Flex objects can have relations to other Flex objects with few lines of code defining the relation +* Flex directories support indexes which allow searching objects without loading all of them +* Efficient caching for indexes, searches, objects and rendered output + +# Limitations and future improvements + +Right now there are a few limitations: + +* Frontend only has a basic routing for the individual pages (you need to do the advanced routing manually by yourself) +* Administration needs more features like filtering, bulk updates etc +* It would be nice to have an easy way to display Flex admin in other admin plugins (it is already possible, but not easy) +* Optional database storage layer would be nice to have +* We need general collection functions to do simple filtering, like: "display all published items" without custom PHP code + +### Notes: + +1. You can actually use pretty much any folder under the `user/` folder of Grav. Simply edit the **Extra Admin Twig Path** option in the `flex-objects.yaml` file. It defaults to `theme://admin/templates` which means it uses the default theme's `admin/templates/` folder if it exists. +2. You can use any path for front end Twig templates also, if you don't want to put them in your theme, you can add an entry in the **Extra Site Twig Path** option of the `flex-objects.yaml` configuration and point to another location. + +# Tricks and tips + +* You can enable and disable directories from **Plugins** > **Flex Objects** + * New Flex Directories can be registered by simply creating a new blueprint file in `user/blueprints/flex-objects` folder + * You can also add types from your plugins by hooking into `onFlexInit` event (see `AccountsServiceProvider` in Grav) +* To properly create your own custom types, you need at least the object blueprint and the template files for collections and objects +* Use `flex-objects.md` page to create entry point for your own directory + * In page header you can use nested `flex.directory` variable to define the directory (or do it in admin) + * In Admin you can just select the directory under the page title + + +# Parameters supported by Flex page type: + +``` +--- +title: 'Flex Directories' +flex: + directories: + layout: default + list: + - accounts + - contacts +--- +``` + +`directories.layout`: uses template file `templates/flex-objects/directories/[LAYOUT].html.twig` +`directories.list`: list of flex directories displayed in this page diff --git a/plugins/flex-objects/admin/templates/flex-objects.html.twig b/plugins/flex-objects/admin/templates/flex-objects.html.twig new file mode 100644 index 0000000..74ee1ca --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects.html.twig @@ -0,0 +1,30 @@ +{%- set user = admin.user -%} +{%- set route = controller.route -%} +{%- set type = directory.config('admin.template') ?? target -%} + +{# Set action from ?preview=1 #} +{%- if key and uri.currentUri().queryParam('preview') %} + {% set action = 'preview' %} +{% endif -%} + +{%- set template -%} + {%- if action == 'add' -%} + edit + {%- elseif action == 'delete' -%} + list + {%- else -%} + {{- action ?: task ?: 'types' -}} + {%- endif -%} +{%- endset -%} + +{%- set separator = config.system.param_sep -%} +{%- set view_config = directory.config('admin.views.' ~ template) ?? directory.config('admin.' ~ template) ?? [] -%} + +{%- include target ? [ + 'flex-objects/types/' ~ type ~ '/' ~ template ~ '.html.twig', + 'flex-objects/types/default/' ~ template ~ '.html.twig', + 'flex-objects/layouts/404.html.twig' + ] : [ + 'flex-objects/types/default/' ~ template ~ '.html.twig', + 'flex-objects/layouts/404.html.twig' + ] -%} diff --git a/plugins/flex-objects/admin/templates/flex-objects.json.twig b/plugins/flex-objects/admin/templates/flex-objects.json.twig new file mode 100644 index 0000000..c5b85dc --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects.json.twig @@ -0,0 +1 @@ +{{- admin.json_response|json_encode|raw -}} \ No newline at end of file diff --git a/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig b/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig new file mode 100644 index 0000000..6101c81 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig @@ -0,0 +1 @@ +{{ 'PLUGIN_FLEX_OBJECTS.ERROR.LAYOUT_NOT_FOUND'|tu(template, null) }} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig new file mode 100644 index 0000000..cb91524 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig @@ -0,0 +1,3 @@ + + {{ 'PLUGIN_ADMIN.ADD'|tu }} + diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig new file mode 100644 index 0000000..dbb57c6 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig @@ -0,0 +1,3 @@ + + {{ "PLUGIN_ADMIN.BACK"|tu }} + diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig new file mode 100644 index 0000000..2a463b2 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig @@ -0,0 +1,7 @@ +{%- set authorize = directory.config('admin.views.configure.authorize') ?? directory.config('admin.configure.authorize') ?? 'admin.super' %} + +{%- if configure_url and user.authorize(authorize) %} + + {{ 'PLUGIN_ADMIN.CONFIGURATION'|tu }} + +{% endif %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig new file mode 100644 index 0000000..779760e --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig @@ -0,0 +1,3 @@ + + {{ 'PLUGIN_ADMIN.DELETE'|tu }} + diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig new file mode 100644 index 0000000..6a8fadc --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig @@ -0,0 +1,3 @@ + + {{ 'PLUGIN_FLEX_OBJECTS.CSV'|tu }} + diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig new file mode 100644 index 0000000..99a36e1 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig @@ -0,0 +1,21 @@ +{% if export.options %} +
    + + +
    + +{% else %} + + {{ export.title ?? (export.formatter.class ? 'PLUGIN_ADMIN.EXPORT'|tu : 'PLUGIN_FLEX_OBJECTS.CSV'|tu) }} + +{% endif %} \ No newline at end of file diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig new file mode 100644 index 0000000..58c2551 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig @@ -0,0 +1,18 @@ +
    + + {% if admin_languages|length > (language in admin_languages)|int %} + + + {% endif %} +
    diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig new file mode 100644 index 0000000..df8893d --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig @@ -0,0 +1,5 @@ +{% if preview_url %} + + {{ "PLUGIN_ADMIN.OPEN_NEW_TAB"|tu }} + +{% endif %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig new file mode 100644 index 0000000..00390a6 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig @@ -0,0 +1,3 @@ + + {{ "PLUGIN_ADMIN.PREVIEW"|tu }} + diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig new file mode 100644 index 0000000..379d97d --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig @@ -0,0 +1,4 @@ +{% set task = task ?? 'save' %} + diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig new file mode 100644 index 0000000..2d72493 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig @@ -0,0 +1,103 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/configure.html.twig' %} + +{% set name = view_config['form'] %} +{% set form = form ?? directory.directoryForm(name) %} + +{# Allowed actions #} +{% set can_save = can_save ?? user.authorize(view_config['authorize'] ?? 'admin.super') %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and form and can_save) %} +{% set back_route = back_route ?? ('/' ~ route.getRoute(1)) %} +{% set title_icon = title_icon ?? view_config['icon'] ?? 'fa-cog' %} +{% set title -%} + {%- set title_config = view_config['title'] ?? null -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'configure title template')) -}} + {%- else -%} + {{- directory.title|tu }} - {{ 'PLUGIN_ADMIN.CONFIGURATION'|tu -}} + {% endif %} +{%- endset %} + +{% macro spanToggle(input, length) %} + {{ (repeat('  ', (length - input|length) / 2) ~ input ~ repeat('  ', (length - input|length) / 2))|raw }} +{% endmacro %} +{% import _self as macro %} + +{% block body %} + {% set back_url = back_url ?? admin_route(back_route) %} + + {{ parent() }} +{% endblock body %} + +{% block content_top %} + {% if allowed and user.authorize('admin.super') %} + {% set save_location = directory.getDirectoryConfigUri(name) %} +
    {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }}
    + {% endif %} +{% endblock %} + +{% block topbar %} + {% if user.authorize('admin.super') %} +
    + {% set normalText = 'PLUGIN_ADMIN.NORMAL'|tu %} + {% set expertText = 'PLUGIN_ADMIN.EXPERT'|tu %} + {% set maxLen = max([normalText|length, expertText|length]) %} + {% set normalText = macro.spanToggle(normalText, maxLen) %} + {% set expertText = macro.spanToggle(expertText, maxLen) %} + +
    + + + + + +
    +
    + {% endif %} +{% endblock topbar %} + +{% block content %} + {{ parent() }} + + {% if allowed %} +
    +
    + {# TODO: RAW MODE +
    + {{ block('topbar') }} +
    + #} + {% block edit %} + {% include 'partials/blueprints.html.twig' with { form: form, data: form.data } %} + {% endblock %} +
    +
    + + {% include 'partials/modal-changes-detected.html.twig' %} + + {% else %} + + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    + + {% endif %} +{% endblock %} + +{% block bottom %} + {{ parent() }} + + {# TODO: RAW MODE + + #} +{% endblock bottom %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig new file mode 100644 index 0000000..ecab064 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig @@ -0,0 +1,121 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/edit.html.twig' %} + +{# Avoid defining form and object twice: object should always come from the form! #} +{% if form is not defined %} + {% set form = object.form %} + {% set object = form.object %} +{% endif %} + +{# Allowed actions #} +{% set can_list = can_list ?? directory.isAuthorized('list', 'admin', user) %} +{% set can_read = can_read ?? (object.exists ? object.isAuthorized('read', 'admin', user) : directory.isAuthorized('create', 'admin', user)) %} +{% set can_create = can_create ?? object.isAuthorized('create', 'admin', user) %} +{% set can_save = can_save ?? (object.exists ? object.isAuthorized('update', 'admin', user) : directory.isAuthorized('create', 'admin', user)) %} +{% set can_delete = can_delete ?? (object.exists and object.isAuthorized('delete', 'admin', user)) %} +{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('flex-translate')) %} +{% set can_preview = can_preview ?? (can_read and object.exists and (directory.config('admin.views.preview.enabled') ?? directory.config('admin.preview.enabled', false))) %} + +{# Translations #} +{% if can_translate %} + {% set translate_include_default = translate_include_default ?? grav.config.get('system.languages.include_default_lang_file_extension', true) %} + {% set all_languages = grav.admin.siteLanguages %} + {% set admin_languages = admin.languages_enabled %} + {% set default_language = grav.language.default %} + {% set object_language = object.language %} + {% set language = controller.language %} + {% set has_translation = object.hasTranslation(language, false) %} + + {# + {% if translate_include_default %} + {% set all_languages = all_languages|merge({'': 'Default'}) %} + {% set admin_languages = admin_languages|merge({'': ''}) %} + {% set object_languages = object.languages(true) %} + {% else %} + #} + {% set language = language ?: default_language %} + {% set object_language = object_language ?: default_language %} + {% set object_languages = object.languages(false) %} + {# endif #} +{% endif %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and (object.exists and (can_read or can_save)) or (action == 'add' and can_read)) %} +{% set back_route = back_route ?? ('/' ~ (action != 'edit' and not key ? route.getRoute(1, not can_list ? -1 : null) : route.getRoute(1, not can_list ? -2 : -1))) %} +{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %} +{% set title -%} + {%- set title_config = view_config['title'] -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'edit title template')) -}} + {%- else -%} + {{- title ?? object.form.getValue('title') ?? object.title ?? key -}} + {% endif %} +{%- endset %} + +{% block body %} + {% set back_url = back_url ?? admin_route(back_route) %} + {% set id = key %} + {% set blueprint = blueprint ?? form.blueprint %} + + {{ parent() }} +{% endblock body %} + +{% block content_top %} + {% if allowed and user.authorize('admin.super') %} + {% if directory and object or action == 'add' %} + {% set save_location = object.getStorageFolder() ?? directory.getStorageFolder() %} +
    {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }} {{ not object.exists ? '[' ~ 'PLUGIN_FLEX_OBJECTS.NEW'|tu|upper ~ ']' }}
    + {% endif %} + {% endif %} + {% if object.exists and form.flash.exists %} +
    + {{ 'PLUGIN_FLEX_OBJECTS.STATE.EDITING_DRAFT'|tu }} +
    + {% endif %} +{% endblock %} + +{% block content %} + {% if allowed %} +
    +
    +
    + {% block topbar %}{% endblock %} +
    + {% block edit %} + {% include 'partials/blueprints.html.twig' with { form: form, context: object, data: object } %} + {% endblock %} +
    +
    + + {% include 'partials/modal-changes-detected.html.twig' %} + + {% if can_delete %} + {% include ['flex-objects/types/' ~ target ~ '/modals/remove.html.twig', 'flex-objects/types/default/modals/remove.html.twig'] with { name: target } %} + {% endif %} + + {% elseif (object.exists) %} + + {% do page.modifyHeader('http_response_code', 403) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 403

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_FORBIDDEN'|tu }} +

    +
    +
    + + {% else %} + + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    + + {% endif %} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig new file mode 100644 index 0000000..abd40a9 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig @@ -0,0 +1,98 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/list.html.twig' %} + +{# Allowed actions #} +{% set export = directory.config('admin.views.export') ?? directory.config('admin.export') ?? [] %} +{% set can_export = can_export ?? (export['enabled'] ?? export|array|count)|bool %} +{% set can_create = can_create ?? directory.isAuthorized('create', 'admin', user) %} +{% set can_translate = can_translate ?? (admin.multilang and directory.object.hasFlexFeature('flex-translate')) %} + +{% set per_page = per_page ?? grav.uri.currentUri.queryParam('per_page') %} + +{# Translations #} +{% if can_translate %} + {% set translate_include_default = translate_include_default ?? grav.config.get('system.languages.include_default_lang_file_extension', true) %} + {% set all_languages = grav.admin.siteLanguages %} + {% set admin_languages = admin.languages_enabled %} + {% set default_language = grav.language.default %} + {% set language = controller.language %} + {# + {% if translate_include_default %} + {% set all_languages = all_languages|merge({'': 'Default'}) %} + {% set admin_languages = admin_languages|merge({'': ''}) %} + {% else %} + #} + {% set language = language ?: default_language %} + {# endif #} +{% endif %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and directory.isAuthorized('list', 'admin', user)) %} +{% set back_route = back_route ?? route.getRoute(1, -1) %} + +{% set configure_path = directory.config('admin.router.actions.configure.path') %} +{% set configure_route = configure_route ?? (configure_path ? route.withRoute(admin_route(configure_path).toString(true)|trim('/')) : null) %} +{% set configure_route = configure_route ?? route.withGravParam('', 'configure').toString(true) %} + +{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %} +{% set title -%} + {%- set title_config = view_config['title'] ?? null -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'configure title template')) -}} + {%- else -%} + {{- directory.title|tu -}} + {% endif %} +{%- endset %} + +{% set schema = directory.blueprint.schema %} + +{% do assets.addJs('plugin://flex-objects/js/flex-objects.js', { 'group': 'bottom', 'loading': 'defer' }) %} + +{% block body %} + {% set collection = directory ? collection.isAuthorized('list', 'admin', user) %} + {% set directory_config = view_config['options'] ?? config.get('plugins.flex-objects.admin_list', { per_page: 15, order: { by: 'updated_timestamp', dir: 'desc' }}) %} + {% set per_page = max(1, per_page ?? directory_config.per_page) %} + {% set table = directory ? flex.dataTable(collection.flexDirectory(), { collection: collection, limit: per_page, sort: directory_config.order.by ~ '|' ~ directory_config.order.dir }) %} + {% set back_url = admin_route(back_route) %} + {% set configure_url = (directory.config('admin.views.configure.hidden') ?? directory.config('admin.configure.hidden', false)) is not same as(true) ? configure_route.toString(true) %} + + {% set fields = table.getColumns() %} + {% set fields_count = fields ? count(fields) : 0 %} + {% set fields_width = 8 %} + {% set fields_set = 0 %} + {% set title_field = view_config['title'] %} + {% for key,options in fields %} + {% set fields_width = fields_width + options.width ?: 0 %} + {% set fields_set = fields_set + (options.width ? 1 : 0) %} + {% if not title_field and options.link == 'edit' %} + {% set title_field = key %} + {% endif %} + {% endfor %} + + {{ parent() }} +{% endblock body %} + +{% block content_top %} +{% if allowed and user.authorize('admin.super') %} + {% set save_location = directory.getStorageFolder() %} +
    {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }}
    +{% endif %} +{% endblock %} + +{% block content %} +{% if allowed %} + {% block content_list %} + {% include ['flex-objects/types/' ~ target ~ '/list/list.html.twig', 'flex-objects/types/default/list/list.html.twig'] %} + {% endblock %} +{% else %} + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    +{% endif %} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig new file mode 100644 index 0000000..2200e53 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig @@ -0,0 +1,112 @@ +{% block directory %} +
    + {% if not fields %} + {% block no_list %} +
    +

    {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST'|tu( target, null )|raw }}

    +
      +
    • + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST_ADVISE'|tu }} +
    • +
    • + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST_TEMPLATE'|tu( target, null )|raw }} +
    • +
    +
    + {% endblock %} + {% elseif not collection.count %} + {% block no_entries %} +
    + {% if directory.isAuthorized('create', 'admin', user) %} + {% set createLink = admin_route(flex.adminRoute(collection, {action: 'add'})) %} + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.LIST_EMPTY_ADD'|tu(createLink, null)|raw }} + {% else %} + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.LIST_EMPTY'|tu }} + {% endif %} +
    + {% endblock %} + {% else %} + {% block entries %} + {% set per_page = per_page ?? directory_config.per_page %} + + {% set tableFields = [] %} + {% set searchFields = [] %} + {% for key, options in fields %} + {% set name = key %} + {% set sortField = options.sort.field ?? key %} + {% set title = (options.field.label ?? schema.get(options.alias ?? key).label)|tu %} + {% set width = options.width ?: ((100-fields_width) / ((fields_count-fields_set) ?: 1))|round(3) %} + {% set title_class = options.title_class ?: '' %} + {% set data_class = options.data_class ?: '' %} + + {# Vuetable doesn't like field names with `.` in them, so we convert name and sortField to `_` #} + {% set tableFields = tableFields|merge([ + { + name: name|replace({'.': '_'}), + sortField: sortField, + title: title ?? 'N/A', + width: width ~ '%', + titleClass: title_class, + dataClass: data_class + } + ]) %} + + {# FIXME: Search fields should be passed and individually customizable, right now defaulting to all fields selected #} + {% set searchFields = searchFields|merge([key|replace({'.': '_'})]) %} + {% endfor %} + {% set tableFields = tableFields|merge([{ name: '_actions_', title: "PLUGIN_FLEX_OBJECTS.ACTION.ACTIONS"|tu, titleClass: 'right' }]) %} + + + {% set list = table.jsonSerialize %} + +
    + + {% for i in 0..((min(per_page, list.data|count) + 3) - 1) %} + + + + + + + + + + + + + + + + + + + + + + + + {% endfor %} + +
    + {% endblock %} + {% endif %} + + {% block modals %} + {% include 'flex-objects/types/default/modals/remove.html.twig' with { name: target } %} + {% endblock %} +
    +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig new file mode 100644 index 0000000..e4d9da5 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig @@ -0,0 +1,39 @@ +{% set object_title = title_field ? "'" ~ object[title_field]|join(' ') ~ "'" : 'Item' %} +{% set can_read = object.isAuthorized('read', 'admin', user) %} +{% set can_update = object.isAuthorized('update', 'admin', user) %} +{% set can_delete = object.isAuthorized('delete', 'admin', user) %} + +{% if can_read and object.getRoute() %} + {% block action_preview %} + + + + {% endblock %} +{% endif %} + +{% if can_update %} + {% block action_edit %} + + + + {% endblock %} +{% elseif can_read %} + {% block action_read %} + + + + {% endblock %} +{% endif %} + +{% if can_delete %} + {% block action_delete %} + + + + {% endblock %} +{% endif %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig new file mode 100644 index 0000000..f37500a --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig @@ -0,0 +1,13 @@ +
    +
    + {# FIXME -name|singularize- is not translatable #} +

    {{ 'PLUGIN_FLEX_OBJECTS.ACTION.DELETE_N'|tu }} {{ name|singularize|capitalize }}

    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ACTION.REALLY_DELETE'|tu( name|singularize, null ) }} +

    +
    + + {{ "PLUGIN_ADMIN.CONTINUE"|tu }} +
    +
    +
    diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig new file mode 100644 index 0000000..3e24744 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig @@ -0,0 +1,62 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/preview.html.twig' %} + +{# Allowed actions #} +{% set can_preview = can_preview ?? (object.exists and (view_config['enabled'] ?? false)) %} +{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('flex-translate')) %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and (object.exists or action == 'add')) %} +{% set back_route = back_route ?? ('/' ~ route.getRoute(1)) %} +{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %} +{% set title -%} + {%- set title_config = view_config['title'] -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'edit title template')) -}} + {%- else -%} + {{- title ?? object.form.getValue('title') ?? object.title ?? key -}} + {% endif %} +{%- endset %} +{% set preview_url -%} + {%- set route_config = view_config['route'] -%} + {%- if route_config.template -%} + {{- include(template_from_string(route_config.template, 'preview route template')) -}} + {%- else -%} + {{- preview_url ?? object.getRoute().uri ?: '' -}} + {%- endif -%} +{% endset -%} + +{% block body %} + {% if not can_preview or not preview_url %} + {% set allowed = false %} + {% endif %} + {% set id = key %} + {% set blueprint = object.blueprint ?? directory.blueprint %} + {% set back_url = back_url ?? admin_route(back_route) %} + + {{ parent() }} +{% endblock body %} + +{% block content_wrapper %} +{% if can_preview and allowed and preview_url %} +
    +
    + +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock content_wrapper %} + +{% block content %} + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig new file mode 100644 index 0000000..08d6f86 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig @@ -0,0 +1,32 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {% block extra_buttons %}{% endblock extra_buttons %} + + {# SAVE #} + {% if can_save %} + {% block save_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/default/buttons/save.html.twig'] with {task: 'configure'} %} + {% endblock save_button %} + {% endif %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ title }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} +   {% endif %} +

    + {% endblock titlebar_title %} + +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig new file mode 100644 index 0000000..b209e44 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig @@ -0,0 +1,46 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {# PREVIEW #} + {% if can_preview %} + {% block preview_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/preview.html.twig', 'flex-objects/types/default/buttons/preview.html.twig'] %} + {% endblock preview_button %} + {% endif %} + + {# DELETE #} + {% if can_delete %} + {% block delete_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/delete.html.twig', 'flex-objects/types/default/buttons/delete.html.twig'] %} + {% endblock delete_button %} + {% endif %} + + {% block extra_buttons %}{% endblock extra_buttons %} + + {# SAVE #} + {% if allowed and can_save %} + {% block save_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/default/buttons/save.html.twig'] with {task: 'save'} %} + {% endblock save_button %} + {% endif %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ not object.exists ? '[' ~ 'PLUGIN_FLEX_OBJECTS.NEW'|tu|upper ~ ']' }} {{ title|tu }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} + {% endif %} +

    + {% endblock titlebar_title %} + +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig new file mode 100644 index 0000000..4397606 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig @@ -0,0 +1,48 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {# EXPORT #} + {% if can_export %} + {% block export_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/export.html.twig', 'flex-objects/types/default/buttons/export.html.twig'] with {export: directory.config('admin.views.export') ?? directory.config('admin.export') ?? []} %} + {% endblock export_button %} + {% endif %} + + {# CREATE #} + {% if can_create %} + {% block create_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/add.html.twig', 'flex-objects/types/default/buttons/add.html.twig'] %} + {% endblock create_button %} + {% endif %} + + {# LANGUAGES #} + {% if can_translate %} + {% block languages_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/languages.html.twig', 'flex-objects/types/default/buttons/languages.html.twig'] %} + {% endblock languages_button %} + {% endif %} + + {# CONFIGURE #} + {% block configure %} + {% include 'flex-objects/types/default/buttons/configuration.html.twig' %} + {% endblock configure %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ directory ? title|tu : 'Error' }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} + {% endif %} +

    + {% endblock titlebar_title %} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig new file mode 100644 index 0000000..6b7bad7 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig @@ -0,0 +1,30 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {# PREVIEW #} + {% if can_preview %} + {% block preview_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/preview-open.html.twig', 'flex-objects/types/default/buttons/preview-open.html.twig'] %} + {% endblock preview_button %} + {% endif %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ "PLUGIN_ADMIN.PREVIEW"|tu }}: {{ not object.exists ? '[' ~ 'PLUGIN_FLEX_OBJECTS.NEW'|tu|upper ~ ']' }} {{ title }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} + {% endif %} +

    + {% endblock titlebar_title %} + +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig new file mode 100644 index 0000000..97789de --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig @@ -0,0 +1,22 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include 'flex-objects/types/default/buttons/back.html.twig' %} + {% endblock back_button %} + + {# CONFIGURE #} + {% block configure %} + {% include 'flex-objects/types/default/buttons/configuration.html.twig' %} + {% endblock configure %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + + {{ "PLUGIN_FLEX_OBJECTS.TITLE"|tu }} +

    + {% endblock titlebar_title %} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig new file mode 100644 index 0000000..88b1ab2 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig @@ -0,0 +1,46 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/types.html.twig' %} + +{% set flex = grav['flex_objects'] %} + +{# These variables can be overridden from the main template file #} +{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %} +{% set configure_route = '/plugins/flex-objects' %} + +{% block body %} + {% set back_url = admin_route(back_route) %} + {% set configure_url = configure_route ? admin_route(configure_route) : null %} + + {{ parent() }} +{% endblock body %} + +{% block content %} + +

    {{ 'PLUGIN_FLEX_OBJECTS.TYPES_TITLE'|tu }}

    + +
    + {% for name,directory in flex.directories if directory.enabled and directory.config('admin.hidden', false) is not same as(true) and not directory.config('admin.menu') %} + {% try %} + {% set collection = directory.collection %} + {% if flex.adminRoute(collection) %} +
    + +

    {{ directory.title|tu }} {{ collection.isAuthorized('list', 'admin', user).count }}

    +

    + {{ directory.description }} +

    +
    + {% endif %} + {% catch %} +
    +

    {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BAD_DIRECTORY'|tu }} '{{ name }}'

    +

    + {{ e.message }} +

    +
    + {% endcatch %} + {% endfor %} + +
    + +{% endblock %} \ No newline at end of file diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig new file mode 100644 index 0000000..2348417 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig @@ -0,0 +1,9 @@ +{% extends 'flex-objects/types/default/configure.html.twig' %} + +{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig new file mode 100644 index 0000000..85fe5fd --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig @@ -0,0 +1,9 @@ +{% extends 'flex-objects/types/default/edit.html.twig' %} + +{% if not directory.isAuthorized('list', 'admin', user) %} + {% set back_route = '/' %} +{% endif %} + +{% if not object.exists %} + {% do object.onPrepareRegistration() %} +{% endif %} \ No newline at end of file diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig new file mode 100644 index 0000000..0cefbb8 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig @@ -0,0 +1,7 @@ +{% extends 'flex-objects/types/default/list.html.twig' %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig new file mode 100644 index 0000000..2348417 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig @@ -0,0 +1,9 @@ +{% extends 'flex-objects/types/default/configure.html.twig' %} + +{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig b/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig new file mode 100644 index 0000000..0cefbb8 --- /dev/null +++ b/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig @@ -0,0 +1,7 @@ +{% extends 'flex-objects/types/default/list.html.twig' %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig b/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig new file mode 100644 index 0000000..ea0ad6a --- /dev/null +++ b/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig @@ -0,0 +1,69 @@ +{% extends "forms/field.html.twig" %} + +{% macro spanToggle(input, length) %} + {% set space = repeat('  ', (length - input|length) / 2) %} + {{ (space ~ input ~ space)|raw }} +{% endmacro %} + +{% import _self as macro %} + +{% set value = (value is null ? field.default : value) %} + +{% block global_attributes %} + {{ parent() }} + data-grav-field-name="{{ (scope ~ field.name)|fieldName }}" +{% endblock %} + +{% block input %} + {% set flex = grav['flex_objects'] %} + {% set all = flex.blueprints %} + {% if all|count %} + {% set legacy = flex.getLegacyBlueprintMap() %} + {% for label, directory in all %} + {% set url = directory.blueprintFile %} + {% set found = url in value %} + {% if not found and legacy[url] is defined %} + {% set found = legacy[url] in value %} + {% endif %} + +
    +
    + {% set maxLen = 0 %} + {% for text in ['PLUGIN_ADMIN.ENABLED', 'PLUGIN_ADMIN.DISABLED'] %} + {% set translation = grav.twig.twig.filters['tu'] is defined ? text|tu : text|t %} + {% set maxLen = max(translation|length, maxLen) %} + {% endfor %} + + {% set id = "toggle_" ~ field.name ~ '_' ~ label %} + + + {% set text = 'PLUGIN_ADMIN.ENABLED' %} + {% set translation = (grav.twig.twig.filters['tu'] is defined ? text|tu : text|t)|trim %} + + + {% set text = 'PLUGIN_ADMIN.DISABLED' %} + {% set translation = (grav.twig.twig.filters['tu'] is defined ? text|tu : text|t)|trim %} + +
    + {{ directory.title|tu }} +
    + {% endfor %} + {% else %} +
    {{ 'PLUGIN_FLEX_OBJECTS.ERROR.NO_FLEX_DIRECTORIES'|tu }}
    + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig b/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig new file mode 100644 index 0000000..92b90c1 --- /dev/null +++ b/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig @@ -0,0 +1,36 @@ +{% extends "forms/field.html.twig" %} + +{% set originalValue = value %} +{% set value = (value is null ? field.default : value) %} +{% set isNew = key ? false : true %} +{% set savedOption = grav.session.post_entries_save|default('edit') %} + +{% if isNew %} + {% set options = {'create-new':'PLUGIN_FLEX_OBJECTS.ACTION.CREATE_NEW', 'edit':'PLUGIN_FLEX_OBJECTS.ACTION.EDIT_ITEM', 'list':'PLUGIN_FLEX_OBJECTS.ACTION.LIST_ITEMS'} %} +{% else %} + {% set options = {'edit':'PLUGIN_FLEX_OBJECTS.ACTION.EDIT_ITEM', 'list':'PLUGIN_FLEX_OBJECTS.ACTION.LIST_ITEMS'} %} +{% endif %} + +{% block input %} + {% for key, text in options %} + {% set id = field.id|default(field.name) ~ '-' ~ key %} + + {% if savedOption == key %} + {% set value = savedOption %} + {% endif %} + + + + + + + + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/plugins/flex-objects/app/columns/finder.js b/plugins/flex-objects/app/columns/finder.js new file mode 100644 index 0000000..ac623b2 --- /dev/null +++ b/plugins/flex-objects/app/columns/finder.js @@ -0,0 +1,425 @@ +import $ from 'jquery'; +import Finder from '../utils/finder'; +import { getInitialRoute, getStore, setInitialRoute } from './index'; +// import getFilters from '../utils/get-filters'; + +let XHRUUID = 0; +const GRAV_CONFIG = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config; + +export const Instances = {}; + +const isInViewport = (elem) => { + const bounding = elem.getBoundingClientRect(); + const titlebar = document.querySelector('#titlebar'); + const offset = titlebar ? titlebar.getBoundingClientRect().height : 0; + return ( + bounding.top >= offset && + bounding.left >= 0 && + bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +}; + +export class FlexPages { + constructor(container, data) { + this.container = $(container); + this.data = data; + const dataLoad = this.dataLoad; + + this.finder = new Finder( + this.container, + (parent, callback) => { + return dataLoad.call(this, parent, callback); + }, + { + labelKey: 'title', + defaultPath: getInitialRoute(), + itemTrigger: '[data-flexpages-expand]', + createItem: function(item) { + return FlexPages.createItem(this.config, item, this); + }, + createItemContent: function(item) { + return FlexPages.createItemContent(this.config, item, this); + } + } + ); + + this.finder.$emitter.on('leaf-selected', (item) => { + setInitialRoute({ + route: item.route.raw + }); + }); + + this.finder.$emitter.on('interior-selected', (item) => { + setInitialRoute({ + route: item.route.raw + }); + }); + + /* + this.finder.$emitter.on('leaf-selected', (item) => { + console.log('selected', item); + this.finder.emit('create-column', () => this.createSimpleColumn(item)); + }); + + this.finder.$emitter.on('item-selected', (selected) => { + console.log('selected', selected); + // for future use only - create column-card creation for file with details like in macOS finder + // this.finder.$emitter('create-column', () => this.createSimpleColumn(selected)); + }); */ + + this.finder.$emitter.on('column-created', () => { + this.container[0].scrollLeft = this.container[0].scrollWidth - this.container[0].clientWidth; + }); + } + + static createItem(config, item, finder) { + const listItem = $('
  • '); + const listItemClasses = [config.className.item]; + // const href = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/'); + const link = $('
    '); + const createItemContent = config.createItemContent || finder.createItemContent; + const fragment = createItemContent.call(this, item); + link.append(fragment) + // .attr('href', href) + .attr('tabindex', -1); + + if (item.url) { + link.attr('href', item.url); + listItemClasses.push(item.className); + } + + if (item[config.childKey]) { + listItemClasses.push(config.className[config.childKey]); + } + + if (item.filters_hit) { + listItemClasses.push('filters-hit'); + } + + listItem.addClass(listItemClasses.join(' ')); + listItem.append(link) + .attr('data-fjs-item', item[config.itemKey]); + + listItem[0]._item = item; + + return listItem; + } + + static createItemContent(config, item) { + const frag = document.createDocumentFragment(); + const route = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/'); + const title = $('
    '); + const link = $(``); + const icon = $(``); + + if (item.extras && item.extras.lang) { + let status = ''; + if (item.extras.translated) { + status = 'translated'; + } + + if (item.extras.lang === 'n/a') { + status = 'not-available'; + } + + const lang = $(`${item.extras.lang}`); + lang.appendTo(icon); + } + + if (item.extras && item.extras && (item.extras.published_date || item.extras.unpublished_date)) { + const clock = $(''); + clock.appendTo(icon); + } + + const info = $(`${item.title} ${item.route.display}`); + const actions = $(''); + + let dotdotdot = null; + if (item.extras) { + const LANG_URL = $('[data-lang-url]').data('langUrl'); + dotdotdot = $('
    '); + dotdotdot.on('click', (event) => { + if (!dotdotdot.find('.dropdown-menu').length) { + let tags = ''; + let langs = ''; + + item.extras.tags.forEach((tag) => { + tags += `${tag}`; + }); + + const translations = item.extras.langs || {}; + Object.keys(translations).forEach((lang) => { + const translated = translations[lang]; + langs += `
    ${lang ? lang : 'default'}`; + }); + + const canPreview = item.extras.actions.includes('preview') && (!(item.extras.tags.includes('non-routable') || item.extras.tags.includes('unpublished'))); + const canEdit = item.extras.actions.includes('edit'); + const canCopy = item.extras.actions.includes('copy'); + const canMove = false; // item.extras.actions.includes('move'); + const canDelete = item.extras.actions.includes('delete'); + const ul = $(``); + ul.appendTo(dotdotdot); + } + + return true; + }); + } + + if (item.child_count) { + const button = $('