diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..635150b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Project exclude paths +/node_modules/ +/target/ \ No newline at end of file diff --git a/.idea/$PRODUCT_WORKSPACE_FILE$ b/.idea/$PRODUCT_WORKSPACE_FILE$ new file mode 100644 index 00000000..d4d7d1e7 --- /dev/null +++ b/.idea/$PRODUCT_WORKSPACE_FILE$ @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + 1.8 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..5c98b428 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/artifacts/solo_war.xml b/.idea/artifacts/solo_war.xml new file mode 100644 index 00000000..50bfaba1 --- /dev/null +++ b/.idea/artifacts/solo_war.xml @@ -0,0 +1,14 @@ + + + $PROJECT_DIR$/target + + + solo + war + + + + + + + \ No newline at end of file diff --git a/.idea/artifacts/solo_war_exploded.xml b/.idea/artifacts/solo_war_exploded.xml new file mode 100644 index 00000000..5a0e07a6 --- /dev/null +++ b/.idea/artifacts/solo_war_exploded.xml @@ -0,0 +1,100 @@ + + + $PROJECT_DIR$/target/solo + + + true + solo + war + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..466cdb35 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..b26911bd --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_beust_jcommander_1_12.xml b/.idea/libraries/Maven__com_beust_jcommander_1_12.xml new file mode 100644 index 00000000..2b9ea172 --- /dev/null +++ b/.idea/libraries/Maven__com_beust_jcommander_1_12.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_github_ben_manes_caffeine_caffeine_2_7_0.xml b/.idea/libraries/Maven__com_github_ben_manes_caffeine_caffeine_2_7_0.xml new file mode 100644 index 00000000..ae576cfe --- /dev/null +++ b/.idea/libraries/Maven__com_github_ben_manes_caffeine_caffeine_2_7_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_google_errorprone_error_prone_annotations_2_3_3.xml b/.idea/libraries/Maven__com_google_errorprone_error_prone_annotations_2_3_3.xml new file mode 100644 index 00000000..1c81eb05 --- /dev/null +++ b/.idea/libraries/Maven__com_google_errorprone_error_prone_annotations_2_3_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_google_protobuf_protobuf_java_3_6_1.xml b/.idea/libraries/Maven__com_google_protobuf_protobuf_java_3_6_1.xml new file mode 100644 index 00000000..233670dd --- /dev/null +++ b/.idea/libraries/Maven__com_google_protobuf_protobuf_java_3_6_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_h2database_h2_1_4_199.xml b/.idea/libraries/Maven__com_h2database_h2_1_4_199.xml new file mode 100644 index 00000000..2fab0ab7 --- /dev/null +++ b/.idea/libraries/Maven__com_h2database_h2_1_4_199.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vdurmont_emoji_java_4_0_0.xml b/.idea/libraries/Maven__com_vdurmont_emoji_java_4_0_0.xml new file mode 100644 index 00000000..a1bfb551 --- /dev/null +++ b/.idea/libraries/Maven__com_vdurmont_emoji_java_4_0_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_0_50_20.xml new file mode 100644 index 00000000..760e2061 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_all_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_all_0_50_20.xml new file mode 100644 index 00000000..fdc6a607 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_all_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_abbreviation_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_abbreviation_0_50_20.xml new file mode 100644 index 00000000..18432751 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_abbreviation_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_admonition_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_admonition_0_50_20.xml new file mode 100644 index 00000000..749b454a --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_admonition_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_anchorlink_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_anchorlink_0_50_20.xml new file mode 100644 index 00000000..f7b83b09 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_anchorlink_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_aside_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_aside_0_50_20.xml new file mode 100644 index 00000000..1f79733b --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_aside_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_attributes_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_attributes_0_50_20.xml new file mode 100644 index 00000000..532f9e9e --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_attributes_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_autolink_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_autolink_0_50_20.xml new file mode 100644 index 00000000..f62c44d5 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_autolink_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_definition_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_definition_0_50_20.xml new file mode 100644 index 00000000..b367b7fc --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_definition_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_emoji_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_emoji_0_50_20.xml new file mode 100644 index 00000000..c5e444db --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_emoji_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_enumerated_reference_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_enumerated_reference_0_50_20.xml new file mode 100644 index 00000000..f41741b2 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_enumerated_reference_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_escaped_character_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_escaped_character_0_50_20.xml new file mode 100644 index 00000000..fe8527de --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_escaped_character_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_footnotes_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_footnotes_0_50_20.xml new file mode 100644 index 00000000..055a8f7c --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_footnotes_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_issues_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_issues_0_50_20.xml new file mode 100644 index 00000000..94f43a06 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_issues_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_strikethrough_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_strikethrough_0_50_20.xml new file mode 100644 index 00000000..f452c1a0 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_strikethrough_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_tables_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_tables_0_50_20.xml new file mode 100644 index 00000000..58aeb033 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_tables_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_tasklist_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_tasklist_0_50_20.xml new file mode 100644 index 00000000..799bcaeb --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_tasklist_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_users_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_users_0_50_20.xml new file mode 100644 index 00000000..c2c391cf --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gfm_users_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gitlab_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gitlab_0_50_20.xml new file mode 100644 index 00000000..b59ae0a6 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_gitlab_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_ins_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_ins_0_50_20.xml new file mode 100644 index 00000000..52319628 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_ins_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_jekyll_front_matter_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_jekyll_front_matter_0_50_20.xml new file mode 100644 index 00000000..809b53fa --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_jekyll_front_matter_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_jekyll_tag_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_jekyll_tag_0_50_20.xml new file mode 100644 index 00000000..56e821f6 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_jekyll_tag_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_macros_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_macros_0_50_20.xml new file mode 100644 index 00000000..d09446f7 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_macros_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_media_tags_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_media_tags_0_50_20.xml new file mode 100644 index 00000000..ac407e4e --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_media_tags_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_superscript_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_superscript_0_50_20.xml new file mode 100644 index 00000000..ef5314bd --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_superscript_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_tables_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_tables_0_50_20.xml new file mode 100644 index 00000000..6de9ffcc --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_tables_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_toc_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_toc_0_50_20.xml new file mode 100644 index 00000000..05fc8d7a --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_toc_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_typographic_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_typographic_0_50_20.xml new file mode 100644 index 00000000..2822e634 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_typographic_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_wikilink_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_wikilink_0_50_20.xml new file mode 100644 index 00000000..14d32406 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_wikilink_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_xwiki_macros_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_xwiki_macros_0_50_20.xml new file mode 100644 index 00000000..ae6f8581 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_xwiki_macros_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_yaml_front_matter_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_yaml_front_matter_0_50_20.xml new file mode 100644 index 00000000..a6657f3a --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_yaml_front_matter_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_youtube_embedded_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_youtube_embedded_0_50_20.xml new file mode 100644 index 00000000..786a2b77 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_ext_youtube_embedded_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_formatter_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_formatter_0_50_20.xml new file mode 100644 index 00000000..111c3d43 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_formatter_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_html2md_converter_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_html2md_converter_0_50_20.xml new file mode 100644 index 00000000..d15e9ed3 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_html2md_converter_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_html_parser_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_html_parser_0_50_20.xml new file mode 100644 index 00000000..e3583583 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_html_parser_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_profile_pegdown_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_profile_pegdown_0_50_20.xml new file mode 100644 index 00000000..ecdd4d29 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_profile_pegdown_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_util_0_50_20.xml b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_util_0_50_20.xml new file mode 100644 index 00000000..9fd2f296 --- /dev/null +++ b/.idea/libraries/Maven__com_vladsch_flexmark_flexmark_util_0_50_20.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__com_zaxxer_HikariCP_3_2_0.xml b/.idea/libraries/Maven__com_zaxxer_HikariCP_3_2_0.xml new file mode 100644 index 00000000..8f760a20 --- /dev/null +++ b/.idea/libraries/Maven__com_zaxxer_HikariCP_3_2_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__commons_cli_commons_cli_1_3_1.xml b/.idea/libraries/Maven__commons_cli_commons_cli_1_3_1.xml new file mode 100644 index 00000000..a1510b98 --- /dev/null +++ b/.idea/libraries/Maven__commons_cli_commons_cli_1_3_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__commons_codec_commons_codec_1_10.xml b/.idea/libraries/Maven__commons_codec_commons_codec_1_10.xml new file mode 100644 index 00000000..27424a17 --- /dev/null +++ b/.idea/libraries/Maven__commons_codec_commons_codec_1_10.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__commons_io_commons_io_2_6.xml b/.idea/libraries/Maven__commons_io_commons_io_2_6.xml new file mode 100644 index 00000000..d722698a --- /dev/null +++ b/.idea/libraries/Maven__commons_io_commons_io_2_6.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml b/.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml new file mode 100644 index 00000000..2ec83767 --- /dev/null +++ b/.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__eu_bitwalker_UserAgentUtils_1_21.xml b/.idea/libraries/Maven__eu_bitwalker_UserAgentUtils_1_21.xml new file mode 100644 index 00000000..474d46f4 --- /dev/null +++ b/.idea/libraries/Maven__eu_bitwalker_UserAgentUtils_1_21.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__javax_servlet_javax_servlet_api_3_1_0.xml b/.idea/libraries/Maven__javax_servlet_javax_servlet_api_3_1_0.xml new file mode 100644 index 00000000..c24f7e30 --- /dev/null +++ b/.idea/libraries/Maven__javax_servlet_javax_servlet_api_3_1_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__junit_junit_3_8_1.xml b/.idea/libraries/Maven__junit_junit_3_8_1.xml new file mode 100644 index 00000000..71b2993d --- /dev/null +++ b/.idea/libraries/Maven__junit_junit_3_8_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__log4j_log4j_1_2_17.xml b/.idea/libraries/Maven__log4j_log4j_1_2_17.xml new file mode 100644 index 00000000..e383c1bf --- /dev/null +++ b/.idea/libraries/Maven__log4j_log4j_1_2_17.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__mysql_mysql_connector_java_8_0_16.xml b/.idea/libraries/Maven__mysql_mysql_connector_java_8_0_16.xml new file mode 100644 index 00000000..b969cc30 --- /dev/null +++ b/.idea/libraries/Maven__mysql_mysql_connector_java_8_0_16.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_apache_commons_commons_pool2_2_4_3.xml b/.idea/libraries/Maven__org_apache_commons_commons_pool2_2_4_3.xml new file mode 100644 index 00000000..cc0b58b6 --- /dev/null +++ b/.idea/libraries/Maven__org_apache_commons_commons_pool2_2_4_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_b3log_latke_core_2_5_7.xml b/.idea/libraries/Maven__org_b3log_latke_core_2_5_7.xml new file mode 100644 index 00000000..d8d60d59 --- /dev/null +++ b/.idea/libraries/Maven__org_b3log_latke_core_2_5_7.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_b3log_latke_repository_h2_2_5_7.xml b/.idea/libraries/Maven__org_b3log_latke_repository_h2_2_5_7.xml new file mode 100644 index 00000000..6f94384e --- /dev/null +++ b/.idea/libraries/Maven__org_b3log_latke_repository_h2_2_5_7.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_b3log_latke_repository_mysql_2_5_7.xml b/.idea/libraries/Maven__org_b3log_latke_repository_mysql_2_5_7.xml new file mode 100644 index 00000000..f544d9f9 --- /dev/null +++ b/.idea/libraries/Maven__org_b3log_latke_repository_mysql_2_5_7.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_beanshell_bsh_2_0b4.xml b/.idea/libraries/Maven__org_beanshell_bsh_2_0b4.xml new file mode 100644 index 00000000..d6f17aa7 --- /dev/null +++ b/.idea/libraries/Maven__org_beanshell_bsh_2_0b4.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_checkerframework_checker_qual_2_6_0.xml b/.idea/libraries/Maven__org_checkerframework_checker_qual_2_6_0.xml new file mode 100644 index 00000000..ef63d5d3 --- /dev/null +++ b/.idea/libraries/Maven__org_checkerframework_checker_qual_2_6_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_http_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_http_9_4_12_v20180830.xml new file mode 100644 index 00000000..eb454dbd --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_http_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_io_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_io_9_4_12_v20180830.xml new file mode 100644 index 00000000..9abaa9bc --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_io_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_security_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_security_9_4_12_v20180830.xml new file mode 100644 index 00000000..ab443001 --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_security_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_server_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_server_9_4_12_v20180830.xml new file mode 100644 index 00000000..dff1cc83 --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_server_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_servlet_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_servlet_9_4_12_v20180830.xml new file mode 100644 index 00000000..669e26d3 --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_servlet_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_util_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_util_9_4_12_v20180830.xml new file mode 100644 index 00000000..d8f913ff --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_util_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_webapp_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_webapp_9_4_12_v20180830.xml new file mode 100644 index 00000000..037b1326 --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_webapp_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_eclipse_jetty_jetty_xml_9_4_12_v20180830.xml b/.idea/libraries/Maven__org_eclipse_jetty_jetty_xml_9_4_12_v20180830.xml new file mode 100644 index 00000000..d76f7e66 --- /dev/null +++ b/.idea/libraries/Maven__org_eclipse_jetty_jetty_xml_9_4_12_v20180830.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_freemarker_freemarker_2_3_28.xml b/.idea/libraries/Maven__org_freemarker_freemarker_2_3_28.xml new file mode 100644 index 00000000..3471c5b0 --- /dev/null +++ b/.idea/libraries/Maven__org_freemarker_freemarker_2_3_28.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_javassist_javassist_3_23_1_GA.xml b/.idea/libraries/Maven__org_javassist_javassist_3_23_1_GA.xml new file mode 100644 index 00000000..7f1d5212 --- /dev/null +++ b/.idea/libraries/Maven__org_javassist_javassist_3_23_1_GA.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_jodd_jodd_core_5_0_3.xml b/.idea/libraries/Maven__org_jodd_jodd_core_5_0_3.xml new file mode 100644 index 00000000..d42c5c59 --- /dev/null +++ b/.idea/libraries/Maven__org_jodd_jodd_core_5_0_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_jodd_jodd_http_5_0_3.xml b/.idea/libraries/Maven__org_jodd_jodd_http_5_0_3.xml new file mode 100644 index 00000000..fdec36b3 --- /dev/null +++ b/.idea/libraries/Maven__org_jodd_jodd_http_5_0_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_jsoup_jsoup_1_9_1.xml b/.idea/libraries/Maven__org_jsoup_jsoup_1_9_1.xml new file mode 100644 index 00000000..5a3014eb --- /dev/null +++ b/.idea/libraries/Maven__org_jsoup_jsoup_1_9_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_nibor_autolink_autolink_0_6_0.xml b/.idea/libraries/Maven__org_nibor_autolink_autolink_0_6_0.xml new file mode 100644 index 00000000..90d8f2b4 --- /dev/null +++ b/.idea/libraries/Maven__org_nibor_autolink_autolink_0_6_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_owasp_encoder_encoder_1_2_2.xml b/.idea/libraries/Maven__org_owasp_encoder_encoder_1_2_2.xml new file mode 100644 index 00000000..c21f7863 --- /dev/null +++ b/.idea/libraries/Maven__org_owasp_encoder_encoder_1_2_2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml b/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml new file mode 100644 index 00000000..20e81636 --- /dev/null +++ b/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_5.xml b/.idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_5.xml new file mode 100644 index 00000000..df1d3833 --- /dev/null +++ b/.idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_5.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_testng_testng_6_1_1.xml b/.idea/libraries/Maven__org_testng_testng_6_1_1.xml new file mode 100644 index 00000000..82c0e67d --- /dev/null +++ b/.idea/libraries/Maven__org_testng_testng_6_1_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__org_yaml_snakeyaml_1_18.xml b/.idea/libraries/Maven__org_yaml_snakeyaml_1_18.xml new file mode 100644 index 00000000..b56e2a02 --- /dev/null +++ b/.idea/libraries/Maven__org_yaml_snakeyaml_1_18.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__redis_clients_jedis_2_9_3.xml b/.idea/libraries/Maven__redis_clients_jedis_2_9_3.xml new file mode 100644 index 00000000..d8dcebb4 --- /dev/null +++ b/.idea/libraries/Maven__redis_clients_jedis_2_9_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..ce8c23fc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..56998fd7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f1e8ad05 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: java +jdk: + - openjdk8 + +install: + - mvn clean install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pci +after_success: + - mvn clean test jacoco:report coveralls:report -Pci diff --git a/CHANGE_LOGS.html b/CHANGE_LOGS.html new file mode 100644 index 00000000..d6d5b550 --- /dev/null +++ b/CHANGE_LOGS.html @@ -0,0 +1,1007 @@ + + + + + + Solo Change Logs + + + + + +

Release 3.6.4 - Aug 19, 2019

+ +

Release 3.6.3 - Jul 13, 2019

+ +

Release 3.6.2 - Jun 7, 2019

+ +

Release 3.6.1 - May 21, 2019

+ +

Release 3.6.0 - Apr 19, 2019

+ +

Release 3.5.0 - Apr 1, 2019

+ +

Release 3.4.0 - Mar 26, 2019

+ +

Release 3.3.0 - Mar 18, 2019

+ +

Release 3.2.0 - Mar 5, 2019

+ +

Release 3.1.0 - Feb 28, 2019

+ +

Release 3.0.0 - Feb 16, 2019

+ +

Release 2.9.9 - Jan 30, 2019

+ +

Release 2.9.8 - Jan 16, 2019

+ +

Release 2.9.7 - Dec 11, 2018

+ +

Release 2.9.6 - Nov 15, 2018

+ +

Release 2.9.5 - Oct 10, 2018

+ +

Release 2.9.4 - Sep 16, 2018

+ +

Release 2.9.3 - Aug 23, 2018

+ +

Release 2.9.2 - Jul 26, 2018

+ +

Release 2.9.1 - Jun 27, 2018

+ +

Release 2.9.0 - May 17, 2018

+ +

Release 2.8.0 - Apr 17, 2018

+ +

Release 2.7.0 - Mar 7, 2018

+ +

Release 2.6.0 - Feb 1, 2018

+ +

Release 2.5.0 - Dec 20, 2017

+ +

Release 2.4.0 - Sep 22, 2017

+ +

Release 2.3.0 - Aug 31, 2017

+ +

Release 2.2.0 - Jul 10, 2017

+ +

Release 2.1.0 - May 25, 2017

+ +

Release 2.0.0 - Apr 14, 2017

+ +

Release 1.9.0 - Feb 21, 2017

+ +

Release 1.8.0 - Jan 23, 2017

+ +

Release 1.7.0 - Nov 9, 2016

+ +

Release 1.6.0 - Sep 8, 2016

+ +

Release 1.5.0 - Aug 10, 2016

+ +

Release 1.4.0 - Jun 28, 2016

+ +

Release 1.3.0 - Dec 19, 2015

+ +

Release 1.2.0 - Dec 2, 2015

+ +

Release 1.1.0 - Oct 1, 2015

+ +

Release 1.0.0 - Sep 16, 2015

+ +

Release 0.6.9 - Jun 28, 2015

+ +

Release 0.6.8 - Mar 24, 2015

+ +

Release 0.6.7 - Oct 16, 2014

+ +

Release 0.6.6 - Apr 26, 2014

+ +

Release 0.6.5 - Nov 1, 2013

+ +

Release 0.6.1 - Aug 25, 2013

+ +

Release 0.6.0 - Apr 26, 2013

+ +

Release 0.5.6 - Feb 19, 2012

+ +

Release 0.5.5 - Nov 24, 2012

+ + +

Release 0.5.0 - Aug 25, 2012

+ +

Release 0.4.6 - Jul 1, 2012

+ +

Release 0.4.5 - Jun 1, 2012

+ +

Release 0.4.1 - Apr 25, 2012

+ +

Release 0.4.0 - Feb 19, 2012

+ +

Release 0.3.5 - Oct 22, 2011

+ +

Release 0.3.1 - Oct 1, 2011

+ + +

Release 0.3.0 - Aug 20, 2011

+ + +

Release 0.2.5 - Apr 30, 2011

+ + +

Release 0.2.5 - Feb 13, 2011

+ + +

Release 0.2.1 - Dec 1, 2010

+ + +

Release 0.2.0 - Nov 12, 2010

+ + +

Release 0.1.1 - Oct 26, 2010

+ + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..20ae4ba1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM maven:3-jdk-8-alpine as MVN_BUILD + +WORKDIR /opt/solo/ +ADD . /tmp +RUN cd /tmp && mvn package -DskipTests -Pci && mv target/solo/* /opt/solo/ \ + && cp -f /tmp/src/main/resources/docker/* /opt/solo/WEB-INF/classes/ + +FROM openjdk:8-alpine +LABEL maintainer="Liang Ding" + +WORKDIR /opt/solo/ +COPY --from=MVN_BUILD /opt/solo/ /opt/solo/ +RUN apk add --no-cache ca-certificates tzdata + +ENV TZ=Asia/Shanghai +EXPOSE 8080 + +ENTRYPOINT [ "java", "-cp", "WEB-INF/lib/*:WEB-INF/classes", "org.b3log.solo.Starter" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bae94e18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..93266cc3 --- /dev/null +++ b/README.md @@ -0,0 +1,303 @@ +

+Solo +

+小而美的博客系统,专为程序员设计 +

+ + + + +
+ + + + + +
+ + + + +

+   +   +   + +

+ +## 💡 简介 + +[Solo](https://github.com/b3log/solo) 是一款小而美的开源博客系统,专为程序员设计。 + +Solo 有着非常活跃的[社区](https://hacpai.com),可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动。 + +> 这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!\ +> 具体细节请浏览 [B3log 构思](https://hacpai.com/article/1546941897596) + +## 🗃 案例 + +* [D 的个人博客](https://88250.b3log.org) +* [Jiahao.Zhang's Blog](https://blog.hduzplus.xyz) +* [子兮子兮](https://zixizixi.cn) +* [铅笔的个人博客](https://pencilso.cn) +* [洗澡狂魔的技术阵地](https://blog.washmoretech.com) +* [liumapp 的个人博客](http://www.liumapp.com) +* [水星的随笔](https://note.abeffect.com) +* [1992 社区](https://1992.cool) +* [DevHyxo](https://blog.devhyxo.top) +* [EchoCow](https://echocow.cn) +* [贼拉正经的技术博客](http://blog.stackoverflow.wiki) +* [记录改变生活](https://www.tuhaoxin.cn) +* [程序员小吴](https://cxyxiaowu.com) +* [潘少的 BLOG](https://taohuawu.club) +* [ZEEKLING](https://blog.zeekling.cn) + +## ✨ 功能 + +* Markdown / Emoji +* [标签聚合分类](https://hacpai.com/article/1558320086126) +* 自定义导航链接 +* 随机文章 / 相关文章 / 置顶 / 更新提醒 +* 自定义文章永久链接 / 签名档 +* 配置站点 SEO 参数 / 公告 / 页脚 +* 代码高亮 / 数学公式 / 流程图 / 五线谱 +* [多皮肤,多端适配](https://github.com/b3log/solo-skins) / [社区皮肤](https://github.com/b3log/solo-third-skins) +* 多语言 / 国际化 +* 友情链接管理 +* 多用户写作,团队博客 +* [Hexo / Jekyll / Markdown 导入](https://hacpai.com/article/1498490209748) +* SQL / JSON / Markdown 导出 +* Atom / RSS / Sitemap +* CDN 静态资源分离 +* [自动同步 GitHub 仓库](https://hacpai.com/article/1557238327458) +* [内置 HTTPS+CDN 文件存储](https://hacpai.com/article/1559928188793) + +## 🎨 界面 + +### 开始使用 + +![start](https://user-images.githubusercontent.com/970828/61179568-96819e80-a637-11e9-8f73-5188d99ba454.png) + +### 后台首页 + +![console](https://user-images.githubusercontent.com/970828/56886176-f210a700-6aa0-11e9-894f-75eda5cf3317.png) + +### 编辑文章 + +![post](https://user-images.githubusercontent.com/970828/56886177-f2a93d80-6aa0-11e9-8a87-14731bdf59e7.png) + +### 选择皮肤 + +![skins](https://user-images.githubusercontent.com/970828/61179517-afd61b00-a636-11e9-87d5-ddea6d6a0fc9.png) + +### 前台界面 + +![Bubble](https://user-images.githubusercontent.com/970828/61182950-2ee64600-a66d-11e9-80d6-676ead6933d9.png) + +![Casper](https://user-images.githubusercontent.com/970828/61182781-d6ae4480-a66a-11e9-84b3-8db55039caa0.png) + +![pinghsu](https://user-images.githubusercontent.com/970828/61182902-86d07d00-a66c-11e9-980a-e75fc6624f98.png) + +![Jane](https://user-images.githubusercontent.com/970828/61182948-25f57480-a66d-11e9-9e33-ded3b7ca87f2.png) + +![nijigen](https://user-images.githubusercontent.com/970828/61182986-e67b5800-a66d-11e9-8dae-7cd94cc16e83.png) + +![timeline](https://user-images.githubusercontent.com/970828/61183026-699cae00-a66e-11e9-9bba-ffff2a264a5d.png) + +## 🍱 皮肤 + +目前内置的皮肤如下,可点击进行预览: + +* [Bubble](https://88250.b3log.org/?skin=Bubble) +* [Casper](https://88250.b3log.org/?skin=Casper) +* [Pinghsu](https://88250.b3log.org/?skin=Pinghsu) +* [Jane](https://88250.b3log.org/?skin=Jane) +* [nijigen](https://88250.b3log.org/?skin=nijigen) +* [Medium](https://88250.b3log.org/?skin=Medium) +* [9IPHP](https://88250.b3log.org/?skin=9IPHP) +* [Andrea](https://88250.b3log.org/?skin=Andrea) +* [Bruce](https://88250.b3log.org/?skin=Bruce) +* [Community](https://88250.b3log.org/?skin=Community) +* [favourite](https://88250.b3log.org/?skin=favourite) +* [Finding](https://88250.b3log.org/?skin=Finding) +* [i-nove](https://88250.b3log.org/?skin=i-nove) +* [metro-hot](https://88250.b3log.org/?skin=metro-hot) +* [NeoEase](https://88250.b3log.org/?skin=NeoEase) +* [next](https://88250.b3log.org/?skin=next) +* [owmx-3.0](https://88250.b3log.org/?skin=owmx-3.0) +* [timeline](https://88250.b3log.org/?skin=timeline) +* [tree-house](https://88250.b3log.org/?skin=tree-house) +* [yilia](https://88250.b3log.org/?skin=yilia) + +欢迎在[该 issue](https://github.com/b3log/solo/issues/12449) 下推荐好看的皮肤,我们会尽量进行制作。 + +## 🛠️ 安装 + +### 本地试用 + +[下载](https://github.com/b3log/solo/releases)最新的 Solo 包解压,进入解压目录执行: + +* Windows: `java -cp "WEB-INF/lib/*;WEB-INF/classes" org.b3log.solo.Starter` +* Unix-like: `java -cp "WEB-INF/lib/*:WEB-INF/classes" org.b3log.solo.Starter` + +如果你有 Java 开发环境,可参考[这里](https://hacpai.com/article/1493822943172)通过源码构建运行。 + +**请注意**:我们不建议通过 war 发布包或者源码构建部署,因为这样的部署方式在将来有新版本发布时升级会比较麻烦。 +这两种方式请仅用于本地试用,线上生产环境建议通过 Docker 部署。 + +### Docker 部署(推荐方案) + +#### 获取最新镜像 + +```shell +docker pull b3log/solo +``` + +#### 启动容器 + +* 使用 MySQL + + 先手动建库(库名 `solo`,字符集使用 `utf8mb4`,排序规则 `utf8mb4_general_ci`),然后启动容器: + + ```shell + docker run --detach --name solo --network=host \ + --env RUNTIME_DB="MYSQL" \ + --env JDBC_USERNAME="root" \ + --env JDBC_PASSWORD="123456" \ + --env JDBC_DRIVER="com.mysql.cj.jdbc.Driver" \ + --env JDBC_URL="jdbc:mysql://127.0.0.1:3306/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC" \ + b3log/solo --listen_port=8080 --server_scheme=http --server_host=localhost --server_port= + ``` + 为了简单,使用了主机网络模式来连接主机上的 MySQL。 + +* 使用 H2 Database + + ```shell + docker run --detach --name solo --volume ~/solo_h2/:/opt/solo/h2/ --publish 8080:8080 \ + --env RUNTIME_DB="H2" \ + --env JDBC_USERNAME="root" \ + --env JDBC_PASSWORD="123456" \ + --env JDBC_DRIVER="org.h2.Driver" \ + --env JDBC_URL="jdbc:h2:/opt/solo/h2/db;MODE=MYSQL" \ + b3log/solo --listen_port=8080 --server_scheme=http --server_host=localhost --server_port= + ``` + +启动参数说明: + +* `--listen_port`:进程监听端口 +* `--server_scheme`:最终访问协议,如果反代服务启用了 HTTPS 这里也需要改为 `https` +* `--server_host`:最终访问域名或公网 IP,不要带端口 +* `--server_port`:最终访问端口,使用浏览器默认的 80 或者 443 的话值留空即可 + +完整启动参数的说明可以使用 `-h` 来查看。 + +#### 日志配置 + +默认通过 log4j 将日志打印到标准输出流,可以通过 `docker logs solo` 进行查看。如果需要覆盖 log4j 配置,可通过挂载文件实现: + +```shell +--volume ~/log4j.properties:/opt/solo/WEB-INF/classes/log4j.properties +``` + +#### 皮肤配置 + +如果要使用其他皮肤,可以挂载目录 skins(里面需要包含所需使用的所有皮肤,官方所有皮肤可从[这里](https://github.com/b3log/solo-skins)下载): + +```shell +--volume ~/skins/:/opt/solo/skins/ +``` + +#### 版本升级 + +1. 拉取最新镜像 +2. 重启容器 + +可参考[这里](https://github.com/b3log/solo/blob/master/scripts/docker-restart.sh)编写一个重启脚本,并通过 crontab 每日凌晨运行来实现自动更新。 + +#### Docker Compose + +请参考[这里](https://github.com/liumapp/solo-in-docker),感谢 [@liumapp](https://github.com/liumapp) 提供。 + +#### ARM/树莓派镜像 + +```shell +docker pull clinan/solo +``` + +感谢 [@Clinan](https://github.com/Clinan) 提供。 + +## 📜 文档 + +* [《提问的智慧》精读注解版](https://hacpai.com/article/1536377163156) +* [从零开始安装 Solo 博客](https://hacpai.com/article/1565021959471)(感谢 [@JInjianh](https://www.jinjianh.com) 分享) +* [用户指南](https://hacpai.com/article/1492881378588) +* [Solo 从设计到实现](https://hacpai.com/article/1537690756242) + * [项目简介](https://hacpai.com/article/1537691255769) + * [搭建开发环境](https://hacpai.com/article/1537694179006) + * [架构理念和约定](https://hacpai.com/article/1537695161321) + * [表结构](https://hacpai.com/article/1537694566175) + * [功能实现](https://hacpai.com/article/1538711714296) + * [实现规范和约定](https://hacpai.com/article/1538711714296) + * [登录验证 GitHub OAuth](https://hacpai.com/article/1554262218912) + * [登录状态和会话](https://hacpai.com/article/1538708370634) + * [发布文章](https://hacpai.com/article/1538893576597) + * [自定义链接路由](https://hacpai.com/article/1543582332567) + * [标签聚合分类](https://hacpai.com/article/1558320086126) + * [发布评论](https://hacpai.com/article/1543642913006) + * [Markdown 渲染](https://hacpai.com/article/1544880962218) + * [导入 Markdown 文章](https://hacpai.com/article/1547643961141) + * 皮肤切换 + * [自动同步 GitHub 仓库](https://hacpai.com/article/1557238327458) + * [内置 HTTPS + CDN 文件存储](https://hacpai.com/article/1559928188793) + * [跨版本升级](https://hacpai.com/article/1560169064267) + * [安全相关](https://hacpai.com/article/1538896576775) + * [错误处理](https://hacpai.com/article/1538901266090) + * 性能相关 + * 数据库设计 + * 查询 SQL + * MySQL 调优 + * 程序缓存 + * Docker 镜像优化 + * 运维相关 + * 开发、生产环境 + * NGINX 反代 + * 动静分离 + * 更好的 Markdown + * 数据备份和迁移 + * 启用 HTTPS + * 通过 Docker 部署实现自动升级 + * 后记 + * 轮子理论 + * 关于开源 + * 现实和理想 + * 博客的未来 +* [皮肤开发指南](https://hacpai.com/article/1493814851007) +* [插件开发](https://docs.google.com/document/pub?id=15H7Q3EBo-44v61Xp_epiYY7vK_gPJLkQaT7T1gkE64w&pli=1) + +## 🏘️ 社区 + +* [讨论区](https://hacpai.com/tag/solo) +* [报告问题](https://github.com/b3log/solo/issues/new/choose) + +## 📄 授权 + +Solo 使用 [GNU Affero General Public License, Version 3](https://www.gnu.org/licenses/agpl-3.0.txt) 开源协议。 + +## 🙏 鸣谢 + +* [jQuery](https://github.com/jquery/jquery):前端 JavaScript 工具库 +* [Vditor](https://github.com/b3log/vditor): 浏览器端的 Markdown 编辑器 +* [Highlight.js](https://github.com/isagalaev/highlight.js):前端代码高亮库 +* [pjax](https://github.com/defunkt/jquery-pjax):pushState + ajax = pjax +* [jsoup](https://github.com/jhy/jsoup):Java HTML 解析器 +* [flexmark](https://github.com/vsch/flexmark-java):Java Markdown 处理库 +* [Apache Commons](http://commons.apache.org):Java 工具库集 +* [Latke](https://github.com/b3log/latke):以 JSON 为主的 Java Web 框架 + +--- + +## 👍 开源项目推荐 + +* 如果你需要搭建一个多用户博客平台,可以考虑使用 [Pipe](https://github.com/b3log/pipe) +* 如果你需要搭建一个社区平台,可以考虑使用 [Sym](https://github.com/b3log/symphony) +* 欢迎加入我们的小众开源社区,详情请看[这里](https://hacpai.com/article/1463025124998) diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..06b09f72 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,138 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file frontend tool. + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.7.0.2, Mar 29, 2019 + */ + +'use strict' +const gulp = require('gulp') +const concat = require('gulp-concat') +const uglify = require('gulp-uglify') +const sass = require('gulp-sass') +const rename = require('gulp-rename') +const del = require('del') + +function sassSkinProcess () { + return gulp.src('./src/main/webapp/skins/*/css/*.scss'). + pipe(sass({ + outputStyle: 'compressed', + includePaths: ['node_modules'] + }).on('error', sass.logError)). + pipe(gulp.dest('./src/main/webapp/skins/')) +} + +function sassWatch () { + gulp.watch(['./src/main/webapp/skins/*/css/*.scss'], sassSkinProcess) + gulp.watch(['./src/main/webapp/scss/*.scss'], sassCommonProcess) +} + +function sassCommonProcess () { + return gulp.src('./src/main/webapp/scss/*.scss'). + pipe(sass({ + outputStyle: 'compressed', + includePaths: ['node_modules'] + }).on('error', sass.logError)). + pipe(gulp.dest('./src/main/webapp/scss/')) +} + +gulp.task('watch', gulp.series(sassWatch)) + +function minJS () { + // minify js + return gulp.src('./src/main/webapp/js/*.js'). + pipe(rename({suffix: '.min'})). + pipe(uglify()). + pipe(gulp.dest('./src/main/webapp/js/')) +} + +function miniAdmin () { + // concat js + const jsJqueryUpload = [ + './src/main/webapp/js/admin/admin.js', + './src/main/webapp/js/admin/editor.js', + './src/main/webapp/js/admin/tablePaginate.js', + './src/main/webapp/js/admin/article.js', + './src/main/webapp/js/admin/comment.js', + './src/main/webapp/js/admin/articleList.js', + './src/main/webapp/js/admin/draftList.js', + './src/main/webapp/js/admin/pageList.js', + './src/main/webapp/js/admin/others.js', + './src/main/webapp/js/admin/linkList.js', + './src/main/webapp/js/admin/preference.js', + './src/main/webapp/js/admin/themeList.js', + './src/main/webapp/js/admin/pluginList.js', + './src/main/webapp/js/admin/userList.js', + './src/main/webapp/js/admin/categoryList.js', + './src/main/webapp/js/admin/commentList.js', + './src/main/webapp/js/admin/plugin.js', + './src/main/webapp/js/admin/main.js', + './src/main/webapp/js/admin/about.js'] + return gulp.src(jsJqueryUpload). + pipe(uglify({output: {ascii_only: true}})). + pipe(concat('admin.min.js')). + pipe(gulp.dest('./src/main/webapp/js/admin')) + +} + +function miniAdminLibs () { + // concat js + const jsJqueryUpload = [ + './src/main/webapp/js/lib/jquery/jquery.min.js', + './src/main/webapp/js/lib/jquery/jquery.bowknot.min.js',] + return gulp.src(jsJqueryUpload). + pipe(uglify({output: {ascii_only: true}})). + // https://github.com/b3log/solo/issues/12522 + pipe(concat('admin-lib.min.js')). + pipe(gulp.dest('./src/main/webapp/js/lib/compress/')) + +} + +function miniPjax () { + // concat js + const jsPjax = [ + './src/main/webapp/js/lib/jquery/jquery-3.1.0.min.js', + './src/main/webapp/js/lib/jquery/jquery.pjax.js', + './src/main/webapp/js/lib/nprogress/nprogress.js'] + return gulp.src(jsPjax). + pipe(uglify()). + pipe(concat('pjax.min.js')). + pipe(gulp.dest('./src/main/webapp/js/lib/compress/')) +} + +function minSkinJS () { + // minify js + return gulp.src('./src/main/webapp/skins/*/js/*.js'). + pipe(rename({suffix: '.min'})). + pipe(uglify()). + pipe(gulp.dest('./src/main/webapp/skins/')) +} + +function cleanProcess () { + return del([ + './src/main/webapp/js/*.min.js', + './src/main/webapp/skins/*/js/*.min.js']) +} + +gulp.task('default', + gulp.series(cleanProcess, sassSkinProcess, sassCommonProcess, gulp.parallel(minSkinJS, minJS), + gulp.parallel(miniPjax, miniAdmin, miniAdminLibs))) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c7f7dac1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8480 @@ +{ + "name": "Solo", + "version": "3.6.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.0.tgz", + "integrity": "sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw==", + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.6.0", + "@babel/helpers": "^7.6.0", + "@babel/parser": "^7.6.0", + "@babel/template": "^7.6.0", + "@babel/traverse": "^7.6.0", + "@babel/types": "^7.6.0", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/parser": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", + "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@babel/generator": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", + "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", + "requires": { + "@babel/types": "^7.6.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", + "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", + "requires": { + "@babel/types": "^7.3.0", + "esutils": "^2.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", + "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.6.0.tgz", + "integrity": "sha512-O1QWBko4fzGju6VoVvrZg0RROCVifcLxiApnGP3OWfWzvxRZFCoBD81K5ur5e3bVY2Vf/5rIJm8cqPKn8HUJng==", + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5", + "@babel/helper-split-export-declaration": "^7.4.4" + } + }, + "@babel/helper-define-map": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", + "integrity": "sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg==", + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/types": "^7.5.5", + "lodash": "^4.17.13" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", + "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", + "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", + "requires": { + "@babel/types": "^7.5.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz", + "integrity": "sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/template": "^7.4.4", + "@babel/types": "^7.5.5", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" + }, + "@babel/helper-regex": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", + "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", + "requires": { + "lodash": "^4.17.13" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", + "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.5.5", + "@babel/types": "^7.5.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-wrap-function": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", + "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.2.0" + } + }, + "@babel/helpers": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.0.tgz", + "integrity": "sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ==", + "requires": { + "@babel/template": "^7.6.0", + "@babel/traverse": "^7.6.0", + "@babel/types": "^7.6.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.3.tgz", + "integrity": "sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w==" + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", + "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz", + "integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.5.5", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.6.0.tgz", + "integrity": "sha512-ZSyYw9trQI50sES6YxREXKu+4b7MAg6Qx2cvyDDYjP2Hpzd3FleOUwC9cqn1+za8d0A2ZU8SHujxFao956efUg==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.6.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-decorators": "^7.2.0" + } + }, + "@babel/plugin-proposal-do-expressions": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.6.0.tgz", + "integrity": "sha512-qJDaoBDbLySwU1tG0jbAomOwz8W1PEiiiK0iLQAnHLr4PYIMVX4ltDGkj3uAKx4HDs1WJ0tozGW1zAQjuTIiWg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-do-expressions": "^7.2.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", + "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0" + } + }, + "@babel/plugin-proposal-export-default-from": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.5.2.tgz", + "integrity": "sha512-wr9Itk05L1/wyyZKVEmXWCdcsp/e185WUNl6AfYZeEKYaUPPvHXRDqO5K1VH7/UamYqGJowFRuCv30aDYZawsg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-export-default-from": "^7.2.0" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.5.2.tgz", + "integrity": "sha512-TKUdOL07anjZEbR1iSxb5WFh810KyObdd29XLFLGo1IDsSuGrjH3ouWSbAxHNmrVKzr9X71UYl2dQ7oGGcRp0g==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-export-namespace-from": "^7.2.0" + } + }, + "@babel/plugin-proposal-function-bind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz", + "integrity": "sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-function-bind": "^7.2.0" + } + }, + "@babel/plugin-proposal-function-sent": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.5.0.tgz", + "integrity": "sha512-JXdfiQpKoC6UgQliZkp3NX7K3MVec1o1nfTWiCCIORE5ag/QZXhL0aSD8/Y2K+hIHonSTxuJF9rh9zsB6hBi2A==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-wrap-function": "^7.2.0", + "@babel/plugin-syntax-function-sent": "^7.2.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", + "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.2.0" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.2.0.tgz", + "integrity": "sha512-0w797xwdPXKk0m3Js74hDi0mCTZplIu93MOSfb1ZLd/XFe3abWypx1QknVk0J+ohnsjYpvjH4Gwfo2i3RicB6Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.2.0" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.4.4.tgz", + "integrity": "sha512-Amph7Epui1Dh/xxUxS2+K22/MUi6+6JVTvy3P58tja3B6yKTSjwwx0/d83rF7551D6PVSSoplQb8GCwqec7HRw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.2.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz", + "integrity": "sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-numeric-separator": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.5.tgz", + "integrity": "sha512-F2DxJJSQ7f64FyTVl5cw/9MWn6naXGdk3Q3UhDbFEEHv+EilCPoeRD3Zh/Utx1CJz4uyKlQ4uH+bJPbEhMV7Zw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.6.0.tgz", + "integrity": "sha512-kj4gkZ6qUggkprRq3Uh5KP8XnE1MdIO0J7MhdDX8+rAbB6dJ2UrensGIS+0NPZAaaJ1Vr0PN6oLUgXMU1uMcSg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.2.0" + } + }, + "@babel/plugin-proposal-pipeline-operator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.5.0.tgz", + "integrity": "sha512-HFYuu/yGnkn69ligXxU0ohOVvQDsMNOUJs/c4PYLUVS6ntCYOyGmRQQaSYJARJ9rvc7/ulZKIzxd4wk91hN63A==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-pipeline-operator": "^7.5.0" + } + }, + "@babel/plugin-proposal-throw-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz", + "integrity": "sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-throw-expressions": "^7.2.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", + "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz", + "integrity": "sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-do-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.2.0.tgz", + "integrity": "sha512-/u4rJ+XEmZkIhspVuKRS+7WLvm7Dky9j9TvGK5IgId8B3FKir9MG+nQxDZ9xLn10QMBvW58dZ6ABe2juSmARjg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", + "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-export-default-from": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.2.0.tgz", + "integrity": "sha512-c7nqUnNST97BWPtoe+Ssi+fJukc9P9/JMZ71IOMNQWza2E+Psrd46N6AEvtw6pqK+gt7ChjXyrw4SPDO79f3Lw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz", + "integrity": "sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.2.0.tgz", + "integrity": "sha512-r6YMuZDWLtLlu0kqIim5o/3TNRAlWb073HwT3e2nKf9I8IIvOggPrnILYPsrrKilmn/mYEMCf/Z07w3yQJF6dg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-function-bind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.2.0.tgz", + "integrity": "sha512-/WzU1lLU2l0wDfB42Wkg6tahrmtBbiD8C4H6EGSX0M4GAjzN6JiOpq/Uh8G6GSoR6lPMvhjM0MNiV6znj6y/zg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-function-sent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.2.0.tgz", + "integrity": "sha512-2MOVuJ6IMAifp2cf0RFkHQaOvHpbBYyWCvgtF/WVqXhTd7Bgtov8iXVCadLXp2FN1BrI2EFl+JXuwXy0qr3KoQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz", + "integrity": "sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", + "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", + "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.2.0.tgz", + "integrity": "sha512-l/NKSlrnvd73/EL540t9hZhcSo4TULBrIPs9Palju8Oc/A8DXDO+xQf04whfeuZLpi8AuIvCAdpKmmubLN4EfQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.2.0.tgz", + "integrity": "sha512-lRCEaKE+LTxDQtgbYajI04ddt6WW0WJq57xqkAZ+s11h4YgfRHhVA/Y2VhfPzzFD4qeLHWg32DMp9HooY4Kqlg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.2.0.tgz", + "integrity": "sha512-DroeVNkO/BnGpL2R7+ZNZqW+E24aR/4YWxP3Qb15d6lPU8KDzF8HlIUIRCOJRn4X77/oyW4mJY+7FHfY82NLtQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz", + "integrity": "sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-pipeline-operator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.5.0.tgz", + "integrity": "sha512-5FVxPiMTMXWk4R7Kq9pt272nDu8VImJdaIzvXFSTcXFbgKWWaOdbic12TvUvl6cK+AE5EgnhwvxuWik4ZYYdzg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-throw-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz", + "integrity": "sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", + "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", + "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", + "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.0.tgz", + "integrity": "sha512-tIt4E23+kw6TgL/edACZwP1OUKrjOTyMrFMLoT5IOFrfMRabCgekjqFd5o6PaAMildBu46oFkekIdMuGkkPEpA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz", + "integrity": "sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-define-map": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5", + "@babel/helper-split-export-declaration": "^7.4.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", + "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz", + "integrity": "sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", + "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", + "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.4.4.tgz", + "integrity": "sha512-WyVedfeEIILYEaWGAUWzVNyqG4sfsNooMhXWsu/YzOvVGcsnPb5PguysjJqI3t3qiaYj0BR8T2f5njdjTGe44Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.2.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", + "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", + "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", + "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", + "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", + "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.6.0.tgz", + "integrity": "sha512-Ma93Ix95PNSEngqomy5LSBMAQvYKVe3dy+JlVJSHEXZR5ASL9lQBedMiCyVtmTLraIDVRE3ZjTZvmXXD2Ozw3g==", + "requires": { + "@babel/helper-module-transforms": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", + "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", + "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.0.tgz", + "integrity": "sha512-jem7uytlmrRl3iCAuQyw8BpB4c4LWvSpvIeXKpMb+7j84lkx4m4mYr5ErAcmN5KM7B6BqrAvRGjBIbbzqCczew==", + "requires": { + "regexp-tree": "^0.1.13" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", + "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", + "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", + "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "requires": { + "@babel/helper-call-delegate": "^7.4.4", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", + "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", + "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", + "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", + "requires": { + "@babel/helper-builder-react-jsx": "^7.3.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", + "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", + "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", + "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", + "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", + "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", + "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", + "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", + "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", + "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", + "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/preset-env": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.6.0.tgz", + "integrity": "sha512-1efzxFv/TcPsNXlRhMzRnkBFMeIqBBgzwmZwlFDw5Ubj0AGLeufxugirwZmkkX/ayi3owsSqoQ4fw8LkfK9SYg==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-dynamic-import": "^7.5.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.5", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.5.0", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.6.0", + "@babel/plugin-transform-classes": "^7.5.5", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.6.0", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/plugin-transform-duplicate-keys": "^7.5.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.4.4", + "@babel/plugin-transform-function-name": "^7.4.4", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-member-expression-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.5.0", + "@babel/plugin-transform-modules-commonjs": "^7.6.0", + "@babel/plugin-transform-modules-systemjs": "^7.5.0", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.6.0", + "@babel/plugin-transform-new-target": "^7.4.4", + "@babel/plugin-transform-object-super": "^7.5.5", + "@babel/plugin-transform-parameters": "^7.4.4", + "@babel/plugin-transform-property-literals": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-reserved-words": "^7.2.0", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.4.4", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.4.4", + "@babel/types": "^7.6.0", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@babel/preset-flow": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.0.0.tgz", + "integrity": "sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-flow-strip-types": "^7.0.0" + } + }, + "@babel/preset-react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", + "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0" + } + }, + "@babel/preset-stage-0": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-stage-0/-/preset-stage-0-7.0.0.tgz", + "integrity": "sha512-FBMd0IiARPtH5aaOFUVki6evHiJQiY0pFy7fizyRF7dtwc+el3nwpzvhb9qBNzceG1OIJModG1xpE0DDFjPXwA==" + }, + "@babel/template": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", + "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0" + }, + "dependencies": { + "@babel/parser": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", + "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" + } + } + }, + "@babel/traverse": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", + "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.6.0", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/parser": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", + "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@babel/types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", + "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@braintree/sanitize-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz", + "integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg==" + }, + "@nodelib/fs.scandir": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.2.tgz", + "integrity": "sha512-wrIBsjA5pl13f0RN4Zx4FNWmU71lv03meGKnqRUoCyan17s4V3WL92f3w3AIuWbNnpcrQyFBU5qMavJoB8d27w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.2", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.2.tgz", + "integrity": "sha512-z8+wGWV2dgUhLqrtRYa03yDx4HWMvXKi1z8g3m2JyxAx8F7xk74asqPk5LAETjqDSGLFML/6CDl0+yFunSYicw==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.3.tgz", + "integrity": "sha512-l6t8xEhfK9Sa4YO5mIRdau7XSOADfmh3jCr0evNHdY+HNkW6xuQhgMH7D73VV6WpZOagrW0UludvMTiifiwTfA==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.2", + "fastq": "^1.6.0" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.4.tgz", + "integrity": "sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ==", + "dev": true + }, + "@types/unist": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", + "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abab": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.1.tgz", + "integrity": "sha512-1zSbbCuoIjafKZ3mblY5ikvAb0ODUbqBnFuUb7f6uLeQhhGJ0vEV4ntmtxKLT2WgXCO94E07BjunsIw1jOMPZw==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "abcjs": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/abcjs/-/abcjs-5.8.1.tgz", + "integrity": "sha512-ZG9mGiPY1oXPH5nkSw+vCVgOik7KFnTuowQXWvD8n1SyhaVdby1rUF4la5CXmbMEH5W6nMn6U4+la2LpHAZLTg==", + "requires": { + "midi": "git+https://github.com/paulrosen/MIDI.js.git#abcjs" + } + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, + "aggregate-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.0.tgz", + "integrity": "sha512-yKD9kEoJIR+2IFqhMwayIBgheLYbB3PS2OBhWae1L/ODTd/JF/30cW0bc9TqzRL3k4U41Dieu3BF4I29p8xesA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^3.2.0" + }, + "dependencies": { + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "requires": { + "buffer-equal": "^1.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "dev": true, + "requires": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "dev": true, + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "dev": true, + "requires": { + "async-done": "^1.2.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "requires": { + "object.assign": "^4.1.0" + } + }, + "babelify": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", + "integrity": "sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==" + }, + "bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "dev": true, + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "bail": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.4.tgz", + "integrity": "sha512-S8vuDB4w6YpRhICUDET3guPlQpaJl7od94tpZ0Fvnyp+MKW/HyDTcRDck+29C9g+d/qQHnddRH3+94kZdrW0Ww==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", + "requires": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } + } + }, + "browserslist": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", + "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", + "requires": { + "caniuse-lite": "^1.0.30000989", + "electron-to-chromium": "^1.3.247", + "node-releases": "^1.1.29" + } + }, + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==" + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30000989", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz", + "integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "ccount": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.4.tgz", + "integrity": "sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "character-entities": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.3.tgz", + "integrity": "sha512-yB4oYSAa9yLcGyTbB4ItFwHw43QHdH129IJ5R+WvxOkWlyFnR5FAaBNnUq4mcxsTVZGh28bHoeTHMKXH1wZf3w==" + }, + "character-entities-html4": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.3.tgz", + "integrity": "sha512-SwnyZ7jQBCRHELk9zf2CN5AnGEc2nA+uKMZLHvcqhpPprjkYhiLn0DywMHgN5ttFZuITMATbh68M6VIVKwJbcg==" + }, + "character-entities-legacy": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.3.tgz", + "integrity": "sha512-YAxUpPoPwxYFsslbdKkhrGnXAtXoHNgYjlBM3WMXkWGTl5RsY3QmOyhwAgL8Nxm9l5LBThXGawxKPn68y6/fww==" + }, + "character-reference-invalid": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz", + "integrity": "sha512-VOq6PRzQBam/8Jm6XBGk2fNEnHXAdGd6go0rtd4weAGECBamHDwwCQSOT12TACIYUZegUXnV6xBXqUssijtxIg==" + }, + "chokidar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", + "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + } + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=" + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" + }, + "cloneable-readable": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", + "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collapse-white-space": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.5.tgz", + "integrity": "sha512-703bOOmytCYAX9cXYqoikYIx6twmFCXsnzRQheBcTG3nzKYBR4P/+wkYeH+Mvj7qUz8zZDtdyzbxfnEi/kYzRQ==" + }, + "collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "dev": true, + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "comma-separated-tokens": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz", + "integrity": "sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ==" + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=" + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "copy-props": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", + "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", + "dev": true, + "requires": { + "each-props": "^1.3.0", + "is-plain-object": "^2.0.1" + } + }, + "core-js-compat": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.2.1.tgz", + "integrity": "sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A==", + "requires": { + "browserslist": "^4.6.6", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "css-b64-images": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz", + "integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=" + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "requires": { + "cssom": "0.3.x" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "d3": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.12.0.tgz", + "integrity": "sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz", + "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.1.tgz", + "integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "dagre-d3-renderer": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz", + "integrity": "sha512-XH2a86isUHRxzIYbjQVEuZtJnWEufb64H5DuXIUmn8esuB40jgLEbUUclulWOW62/ZoXlj2ZDyL8SJ+YRxs+jQ==", + "requires": { + "dagre-layout": "^0.8.8", + "lodash": "^4.17.5" + } + }, + "dagre-layout": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dagre-layout/-/dagre-layout-0.8.8.tgz", + "integrity": "sha512-ZNV15T9za7X+fV8Z07IZquUKugCxm5owoiPPxfEx6OJRD331nkiIaF3vSt0JEY5FkrY0KfRQxcpQ3SpXB7pLPQ==", + "requires": { + "graphlibrary": "^2.2.0", + "lodash": "^4.17.5" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "dev": true, + "requires": { + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "del": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", + "integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==", + "dev": true, + "requires": { + "globby": "^10.0.1", + "graceful-fs": "^4.2.2", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.1", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "detab": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.2.tgz", + "integrity": "sha512-Q57yPrxScy816TTE1P/uLRXLDKjXhvYTbfxS/e6lPD+YrqghbsMlGB9nQzj/zVtSPaF0DFPSdO916EWO4sQUyQ==", + "requires": { + "repeat-string": "^1.5.4" + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "requires": { + "acorn": "^5.2.1", + "defined": "^1.0.0" + } + }, + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==" + }, + "diff-match-patch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", + "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "doctrine-temporary-fork": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine-temporary-fork/-/doctrine-temporary-fork-2.1.0.tgz", + "integrity": "sha512-nliqOv5NkE4zMON4UA6AMJE6As35afs8aYXATpU4pTUdIKiARZwrJVEP1boA3Rx1ZXHVkwxkhcq4VkqvsuRLsA==", + "requires": { + "esutils": "^2.0.2" + } + }, + "documentation": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-12.1.2.tgz", + "integrity": "sha512-k0orsM458oU4m2P7yaXuLwM/0TPC1y0AWeiM5qLIT39oGjUgHLY+VOlb9x2cty5LyENxLQl4rtQzTMWKJ6l5Ng==", + "requires": { + "@babel/core": "^7.1.2", + "@babel/generator": "^7.1.3", + "@babel/parser": "7.1.3", + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-proposal-decorators": "^7.1.2", + "@babel/plugin-proposal-do-expressions": "^7.0.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-export-namespace-from": "^7.0.0", + "@babel/plugin-proposal-function-bind": "^7.0.0", + "@babel/plugin-proposal-function-sent": "^7.1.0", + "@babel/plugin-proposal-json-strings": "^7.0.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.0.0", + "@babel/plugin-proposal-pipeline-operator": "^7.0.0", + "@babel/plugin-proposal-throw-expressions": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-syntax-import-meta": "^7.0.0", + "@babel/preset-env": "^7.1.0", + "@babel/preset-flow": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "@babel/preset-stage-0": "^7.0.0", + "@babel/traverse": "^7.1.4", + "@babel/types": "^7.1.3", + "ansi-html": "^0.0.7", + "babelify": "^10.0.0", + "chalk": "^2.3.0", + "chokidar": "^2.0.4", + "concat-stream": "^1.6.0", + "diff": "^4.0.1", + "doctrine-temporary-fork": "2.1.0", + "get-port": "^4.0.0", + "git-url-parse": "^10.0.1", + "github-slugger": "1.2.0", + "glob": "^7.1.2", + "globals-docs": "^2.4.0", + "highlight.js": "^9.15.5", + "ini": "^1.3.5", + "js-yaml": "^3.10.0", + "lodash": "^4.17.10", + "mdast-util-inject": "^1.1.0", + "micromatch": "^3.1.5", + "mime": "^2.2.0", + "module-deps-sortable": "5.0.0", + "parse-filepath": "^1.0.2", + "pify": "^4.0.0", + "read-pkg-up": "^4.0.0", + "remark": "^9.0.0", + "remark-html": "^8.0.0", + "remark-reference-links": "^4.0.1", + "remark-toc": "^5.0.0", + "resolve": "^1.8.1", + "stream-array": "^1.1.2", + "strip-json-comments": "^2.0.1", + "tiny-lr": "^1.1.0", + "unist-builder": "^1.0.2", + "unist-util-visit": "^1.3.0", + "vfile": "^4.0.0", + "vfile-reporter": "^6.0.0", + "vfile-sort": "^2.1.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.2", + "vue-template-compiler": "^2.5.16", + "yargs": "^12.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "echarts": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-4.3.0.tgz", + "integrity": "sha512-tX2dAyhI9D78eVrlKhqJGdrM7Ku599HHvPxT2Pu6tgb/dBP8tYmkWjn8r1Ea9oABQxAKXr0p2/tG1OzKdzULbw==", + "requires": { + "zrender": "4.1.0" + } + }, + "electron-to-chromium": { + "version": "1.3.261", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.261.tgz", + "integrity": "sha512-nA9xFj1SgHXlW/6T4+udW2u/Ic/bne25UoFsddoNM7Ut2bgNGLLQUhLhk+vQlbKh9WiRgDDlHaC36Oy7AgRR9w==" + }, + "emoji-regex": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz", + "integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + } + } + }, + "error": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/error/-/error-7.2.0.tgz", + "integrity": "sha512-M6t3j3Vt3uDicrViMP5fLq2AeADNrCVFD8Oj4Qt2MHsX0mPYG7D5XdnEfSdRpaHQzjAJ19wu+I1mw9rQYMTAPg==", + "requires": { + "string-template": "~0.2.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es5-ext": { + "version": "0.10.51", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.51.tgz", + "integrity": "sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "^1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escaper": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz", + "integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ==" + }, + "escodegen": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", + "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz", + "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.1", + "@nodelib/fs.walk": "^1.2.1", + "glob-parent": "^5.0.0", + "is-glob": "^4.0.1", + "merge2": "^1.2.3", + "micromatch": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fastq": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", + "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", + "dev": true, + "requires": { + "reusify": "^1.0.0" + } + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-2.1.0.tgz", + "integrity": "sha512-MJgwfcSd9qxgDyEYpRU/CDxNpUadrK80JHuEQDG4Urn0m7tpSOgCBrtiSIa9S9KH8Tbuo/TN8SSQmJBvsw1HkA==", + "requires": { + "is-ssh": "^1.3.0", + "parse-url": "^3.0.2" + } + }, + "git-url-parse": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-10.1.0.tgz", + "integrity": "sha512-goZOORAtFjU1iG+4zZgWq+N7It09PqS3Xsy43ZwhP5unDD0tTSmXTpqULHodMdJXGejm3COwXIhIRT6Z8DYVZQ==", + "requires": { + "git-up": "^2.0.0" + } + }, + "github-slugger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.2.0.tgz", + "integrity": "sha512-wIaa75k1vZhyPm9yWrD08A5Xnx/V+RmzGrpjQuLemGKSb77Qukiaei58Bogrl/LZSADDfPzKJX8jhLs4CRTl7Q==", + "requires": { + "emoji-regex": ">=6.0.0 <=6.1.1" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "glob-watcher": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", + "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "object.defaults": "^1.1.0" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globals-docs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/globals-docs/-/globals-docs-2.4.1.tgz", + "integrity": "sha512-qpPnUKkWnz8NESjrCvnlGklsgiQzlq+rcCxoG5uNQ+dNA7cFMCmn231slLAwS2N/PlkzZ3COL8CcS10jXmLHqg==" + }, + "globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "glogg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", + "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graphlibrary": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/graphlibrary/-/graphlibrary-2.2.0.tgz", + "integrity": "sha512-XTcvT55L8u4MBZrM37zXoUxsgxs/7sow7YSygd9CIwfWTVO8RVu7AYXhhCiTuFEf+APKgx6Jk4SuQbYR0vYKmQ==", + "requires": { + "lodash": "^4.17.5" + } + }, + "gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "dev": true, + "requires": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "dependencies": { + "gulp-cli": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", + "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.1.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.0.1", + "yargs": "^7.1.0" + } + } + } + }, + "gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", + "dev": true, + "requires": { + "concat-with-sourcemaps": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^2.0.0" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + } + } + }, + "gulp-rename": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz", + "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==", + "dev": true + }, + "gulp-sass": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-4.0.2.tgz", + "integrity": "sha512-q8psj4+aDrblJMMtRxihNBdovfzGrXJp1l4JU0Sz4b/Mhsi2DPrKFYCGDwjIWRENs04ELVHxdOJQ7Vs98OFohg==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "lodash.clonedeep": "^4.3.2", + "node-sass": "^4.8.3", + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "strip-ansi": "^4.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "gulp-uglify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gulp-uglify/-/gulp-uglify-3.0.2.tgz", + "integrity": "sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg==", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "extend-shallow": "^3.0.2", + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "isobject": "^3.0.1", + "make-error-cause": "^1.1.1", + "safe-buffer": "^5.1.2", + "through2": "^2.0.0", + "uglify-js": "^3.0.5", + "vinyl-sourcemaps-apply": "^0.2.0" + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hast-util-is-element": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.3.tgz", + "integrity": "sha512-C62CVn7jbjp89yOhhy7vrkSaB7Vk906Gtcw/Ihd+Iufnq+2pwOZjdPmpzpKLWJXPJBMDX3wXg4FqmdOayPcewA==" + }, + "hast-util-sanitize": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-1.3.1.tgz", + "integrity": "sha512-AIeKHuHx0Wk45nSkGVa2/ujQYTksnDl8gmmKo/mwQi7ag7IBZ8cM3nJ2G86SajbjGP/HRpud6kMkPtcM2i0Tlw==", + "requires": { + "xtend": "^4.0.1" + } + }, + "hast-util-to-html": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-4.0.1.tgz", + "integrity": "sha512-2emzwyf0xEsc4TBIPmDJmBttIw8R4SXAJiJZoiRR/s47ODYWgOqNoDbf2SJAbMbfNdFWMiCSOrI3OVnX6Qq2Mg==", + "requires": { + "ccount": "^1.0.0", + "comma-separated-tokens": "^1.0.1", + "hast-util-is-element": "^1.0.0", + "hast-util-whitespace": "^1.0.0", + "html-void-elements": "^1.0.0", + "property-information": "^4.0.0", + "space-separated-tokens": "^1.0.0", + "stringify-entities": "^1.0.1", + "unist-util-is": "^2.0.0", + "xtend": "^4.0.1" + }, + "dependencies": { + "unist-util-is": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.3.tgz", + "integrity": "sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA==" + } + } + }, + "hast-util-whitespace": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.3.tgz", + "integrity": "sha512-AlkYiLTTwPOyxZ8axq2/bCwRUPjIPBfrHkXuCR92B38b3lSdU22R5F/Z4DL6a2kxWpekWq1w6Nj48tWat6GeRA==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "highlight.js": { + "version": "9.15.10", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz", + "integrity": "sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==" + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "requires": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + } + }, + "html-void-elements": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.4.tgz", + "integrity": "sha512-yMk3naGPLrfvUV9TdDbuYXngh/TpHbA6TrOw3HL9kS8yhwx7i309BReNg7CbAJXGE+UMJ6je5OqJ7lC63o6YuQ==" + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-alphabetical": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.3.tgz", + "integrity": "sha512-eEMa6MKpHFzw38eKm56iNNi6GJ7lf6aLLio7Kr23sJPAECscgRtZvOBYybejWDQ2bM949Y++61PY+udzj5QMLA==" + }, + "is-alphanumeric": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", + "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=" + }, + "is-alphanumerical": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.3.tgz", + "integrity": "sha512-A1IGAPO5AW9vSh7omxIlOGwIqEvpW/TA+DksVOPM5ODuxKlZS09+TEM1E3275lJqO2oJ38vDpeAL3DCIiHE6eA==", + "requires": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-decimal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.3.tgz", + "integrity": "sha512-bvLSwoDg2q6Gf+E2LEPiklHZxxiSi3XAh4Mav65mKqTfCO1HM3uBs24TjEH8iJX3bbDdLXKJXBTmGzuTUuAEjQ==" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-hexadecimal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.3.tgz", + "integrity": "sha512-zxQ9//Q3D/34poZf8fiy3m3XVpbQc7ren15iKqrTtLPwkPD/t3Scy9Imp63FujULGxuK0ZlCwoo5xNpktFgbOA==" + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.1.tgz", + "integrity": "sha512-CKstxrctq1kUesU6WhtZDbYKzzYBuRH0UYInAVrkc/EYdB9ltbfE0gOoayG9nhohG6447sOOVGhHqsdmBvkbNg==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-ssh": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.1.tgz", + "integrity": "sha512-0eRIASHZt1E68/ixClI8bp2YK2wmBPVWEismTs6M+M099jKgrzl/3E976zIbImSIob48N2/XGe9y7ZiYdImSlg==", + "requires": { + "protocols": "^1.1.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=" + }, + "is-whitespace-character": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.3.tgz", + "integrity": "sha512-SNPgMLz9JzPccD3nPctcj8sZlX9DAMJSKH8bP7Z6bohCwuNgX8xbWr1eTAYXX9Vpi/aSn8Y1akL9WgM3t43YNQ==" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-word-character": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.3.tgz", + "integrity": "sha512-0wfcrFgOOOBdgRNT9H33xe6Zi6yhX/uoc4U8NBZGeQQB0ctU1dnlNTyL9JM2646bHDTpsDm1Brb3VPoCIMrd/A==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "just-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", + "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", + "dev": true + }, + "katex": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.11.0.tgz", + "integrity": "sha512-RQsU3HSMjLW9AdPpi2zaBwM123goCbUcUbBJfmjcAdA982RgtEUNMmrf+3y8anGjgJLantcNLa/VSK73xztQBg==", + "requires": { + "commander": "^2.19.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "dev": true, + "requires": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "requires": { + "flush-write-stream": "^1.0.2" + } + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==" + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "longest-streak": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.3.tgz", + "integrity": "sha512-9lz5IVdpwsKLMzQi0MQ+oD9EA0mIGcWYP7jXMTZVXP8D42PwuAk+M/HBFYQoxt1G5OR8m7aSIgb1UymfWGBWEw==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "make-error-cause": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-1.2.2.tgz", + "integrity": "sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0=", + "dev": true, + "requires": { + "make-error": "^1.2.0" + } + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-escapes": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.3.tgz", + "integrity": "sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==" + }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==" + }, + "matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "dev": true, + "requires": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "mdast-util-compact": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.3.tgz", + "integrity": "sha512-nRiU5GpNy62rZppDKbLwhhtw5DXoFMqw9UNZFmlPsNaQCZ//WLjGKUwWMdJrUH+Se7UvtO2gXtAMe0g/N+eI5w==", + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "mdast-util-definitions": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-1.2.4.tgz", + "integrity": "sha512-HfUArPog1j4Z78Xlzy9Q4aHLnrF/7fb57cooTHypyGoe2XFNbcx/kWZDoOz+ra8CkUzvg3+VHV434yqEd1DRmA==", + "requires": { + "unist-util-visit": "^1.0.0" + } + }, + "mdast-util-inject": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", + "integrity": "sha1-2wa4tYW+lZotzS+H9HK6m3VvNnU=", + "requires": { + "mdast-util-to-string": "^1.0.0" + } + }, + "mdast-util-to-hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-3.0.4.tgz", + "integrity": "sha512-/eIbly2YmyVgpJNo+bFLLMCI1XgolO/Ffowhf+pHDq3X4/V6FntC9sGQCDLM147eTS+uSXv5dRzJyFn+o0tazA==", + "requires": { + "collapse-white-space": "^1.0.0", + "detab": "^2.0.0", + "mdast-util-definitions": "^1.2.0", + "mdurl": "^1.0.1", + "trim": "0.0.1", + "trim-lines": "^1.0.0", + "unist-builder": "^1.0.1", + "unist-util-generated": "^1.1.0", + "unist-util-position": "^3.0.0", + "unist-util-visit": "^1.1.0", + "xtend": "^4.0.1" + } + }, + "mdast-util-to-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.0.6.tgz", + "integrity": "sha512-868pp48gUPmZIhfKrLbaDneuzGiw3OTDjHc5M1kAepR2CWBJ+HpEsm252K4aXdiP5coVZaJPOqGtVU6Po8xnXg==" + }, + "mdast-util-toc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-3.1.0.tgz", + "integrity": "sha512-Za0hqL1PqWrvxGtA/3NH9D5nhGAUS9grMM4obEAz5+zsk1RIw/vWUchkaoDLNdrwk05A0CSC5eEXng36/1qE5w==", + "requires": { + "github-slugger": "^1.2.1", + "mdast-util-to-string": "^1.0.5", + "unist-util-is": "^2.1.2", + "unist-util-visit": "^1.1.0" + }, + "dependencies": { + "github-slugger": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.2.1.tgz", + "integrity": "sha512-SsZUjg/P03KPzQBt7OxJPasGw6NRO5uOgiZ5RGXVud5iSIZ0eNZeNp5rTwCxtavrRUa/A77j8mePVc5lEvk0KQ==", + "requires": { + "emoji-regex": ">=6.0.0 <=6.1.1" + } + }, + "unist-util-is": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.3.tgz", + "integrity": "sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA==" + } + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "merge2": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.4.tgz", + "integrity": "sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==", + "dev": true + }, + "mermaid": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.2.6.tgz", + "integrity": "sha512-A8y4zW2aXPj8Yw+BkrCkV6fvzhsFWVESV1IkzRjqQ6T/+tzhkz946+bdebCmHqicEJGTncu/U6h8dgjo5pWo6Q==", + "requires": { + "@braintree/sanitize-url": "^3.1.0", + "d3": "^5.7.0", + "dagre-d3-renderer": "^0.5.8", + "dagre-layout": "^0.8.8", + "documentation": "^12.0.1", + "graphlibrary": "^2.2.0", + "he": "^1.2.0", + "lodash": "^4.17.11", + "minify": "^4.1.1", + "moment-mini": "^2.22.1", + "scope-css": "^1.2.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "midi": { + "version": "git+https://github.com/paulrosen/MIDI.js.git#e593ffef81a0350f99448e3ab8111957145ff6b2", + "from": "git+https://github.com/paulrosen/MIDI.js.git#abcjs" + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "requires": { + "mime-db": "~1.36.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz", + "integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==", + "requires": { + "clean-css": "^4.1.6", + "css-b64-images": "~0.2.5", + "debug": "^4.1.0", + "html-minifier": "^4.0.0", + "terser": "^4.0.0", + "try-catch": "^2.0.0", + "try-to-catch": "^1.0.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "module-deps-sortable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/module-deps-sortable/-/module-deps-sortable-5.0.0.tgz", + "integrity": "sha512-bnGGeghQmz/t/6771/KC4FmxpVm126iR6AAzzq4N6hVZQVl4+ZZBv+VF3PJmDyxXtVtgcgTSSP7NL+jq1QAHrg==", + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.5.0", + "defined": "^1.0.0", + "detective": "^4.0.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "readable-stream": "^2.0.2", + "resolve": "^1.1.3", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + } + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "moment-mini": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.22.1.tgz", + "integrity": "sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "dev": true + }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + } + } + }, + "node-releases": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.32.tgz", + "integrity": "sha512-VhVknkitq8dqtWoluagsGPn3dxTvN9fwgR59fV3D7sLBHe0JfDramsMI8n8mY//ccq/Kkrf8ZRHRpsyVZ3qw1A==", + "requires": { + "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "requires": { + "once": "^1.3.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nwsapi": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", + "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse-path": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-3.0.4.tgz", + "integrity": "sha512-wP70vtwv2DyrM2YoA7ZHVv4zIXa4P7dGgHlj+VwyXNDduLLVJ7NMY1zsFxjUUJ3DAwJLupGb1H5gMDDiNlJaxw==", + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + } + }, + "parse-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-3.0.2.tgz", + "integrity": "sha1-YCeHpwY6eV1yuGcxl1BecvYGEL4=", + "requires": { + "is-ssh": "^1.3.0", + "normalize-url": "^1.9.1", + "parse-path": "^3.0.1", + "protocols": "^1.4.0" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + } + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "property-information": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-4.2.0.tgz", + "integrity": "sha512-TlgDPagHh+eBKOnH2VYvk8qbwsCG/TAJdmTL7f1PROUcSO8qt/KSmShEQ/OKvock8X9tFjtqjCScyOkkkvIKVQ==", + "requires": { + "xtend": "^4.0.1" + } + }, + "protocols": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz", + "integrity": "sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "dev": true + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", + "requires": { + "bytes": "1", + "string_decoder": "0.10" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "requires": { + "private": "^0.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp-tree": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.13.tgz", + "integrity": "sha512-hwdV/GQY5F8ReLZWO+W1SRoN5YfpOKY6852+tBFcma72DKBIcHjPRIlIvQN35bCOljuAfP2G2iB0FC/w236mUw==" + }, + "regexpu-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "remark": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-9.0.0.tgz", + "integrity": "sha512-amw8rGdD5lHbMEakiEsllmkdBP+/KpjW/PRK6NSGPZKCQowh0BT4IWXDAkRMyG3SB9dKPXWMviFjNusXzXNn3A==", + "requires": { + "remark-parse": "^5.0.0", + "remark-stringify": "^5.0.0", + "unified": "^6.0.0" + } + }, + "remark-html": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-8.0.0.tgz", + "integrity": "sha512-3V2391GL3hxKhrkzYOyfPpxJ6taIKLCfuLVqumeWQOk3H9nTtSQ8St8kMYkBVIEAquXN1chT83qJ/2lAW+dpEg==", + "requires": { + "hast-util-sanitize": "^1.0.0", + "hast-util-to-html": "^4.0.0", + "mdast-util-to-hast": "^3.0.0", + "xtend": "^4.0.1" + } + }, + "remark-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz", + "integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==", + "requires": { + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^1.1.0", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^1.0.0", + "vfile-location": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "remark-reference-links": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/remark-reference-links/-/remark-reference-links-4.0.4.tgz", + "integrity": "sha512-+2X8hwSQqxG4tvjYZNrTcEC+bXp8shQvwRGG6J/rnFTvBoU4G0BBviZoqKGZizLh/DG+0gSYhiDDWCqyxXW1iQ==", + "requires": { + "unist-util-visit": "^1.0.0" + } + }, + "remark-slug": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-5.1.2.tgz", + "integrity": "sha512-DWX+Kd9iKycqyD+/B+gEFO3jjnt7Yg1O05lygYSNTe5i5PIxxxPjp5qPBDxPIzp5wreF7+1ROCwRgjEcqmzr3A==", + "requires": { + "github-slugger": "^1.0.0", + "mdast-util-to-string": "^1.0.0", + "unist-util-visit": "^1.0.0" + } + }, + "remark-stringify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-5.0.0.tgz", + "integrity": "sha512-Ws5MdA69ftqQ/yhRF9XhVV29mhxbfGhbz0Rx5bQH+oJcNhhSM6nCu1EpLod+DjrFGrU0BMPs+czVmJZU7xiS7w==", + "requires": { + "ccount": "^1.0.0", + "is-alphanumeric": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "longest-streak": "^2.0.1", + "markdown-escapes": "^1.0.0", + "markdown-table": "^1.1.0", + "mdast-util-compact": "^1.0.0", + "parse-entities": "^1.0.2", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "stringify-entities": "^1.0.1", + "unherit": "^1.0.4", + "xtend": "^4.0.1" + } + }, + "remark-toc": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/remark-toc/-/remark-toc-5.1.1.tgz", + "integrity": "sha512-vCPW4YOsm2CfyuScdktM9KDnJXVHJsd/ZeRtst+dnBU3B3KKvt8bc+bs5syJjyptAHfqo7H+5Uhz+2blWBfwow==", + "requires": { + "mdast-util-toc": "^3.0.0", + "remark-slug": "^5.0.0" + } + }, + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + } + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", + "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "requires": { + "value-or-function": "^3.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "scope-css": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/scope-css/-/scope-css-1.2.1.tgz", + "integrity": "sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==", + "requires": { + "escaper": "^2.5.3", + "slugify": "^1.3.1", + "strip-css-comments": "^3.0.0" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" + }, + "semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "dev": true, + "requires": { + "sver-compat": "^1.5.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slugify": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.5.tgz", + "integrity": "sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "space-separated-tokens": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.4.tgz", + "integrity": "sha512-UyhMSmeIqZrQn2UdjYpxEkwY9JUrn8pP+7L4f91zRzOQuI8MF1FGLfYU9DKCYeLdo7LXMxwrX5zKFy7eeeVHuA==" + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, + "state-toggle": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.2.tgz", + "integrity": "sha512-8LpelPGR0qQM4PnfLiplOQNJcIN1/r2Gy0xKB2zKnIW2YzPMt2sR4I/+gtPjhN7Svh9kw+zqEg2SFwpBO9iNiw==" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/stream-array/-/stream-array-1.1.2.tgz", + "integrity": "sha1-nl9zRfITfDDuO0mLkRToC1K7frU=", + "requires": { + "readable-stream": "~2.1.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "readable-stream": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", + "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", + "requires": { + "buffer-shims": "^1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringify-entities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", + "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "requires": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-css-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-3.0.0.tgz", + "integrity": "sha1-elYl7/iisibPiUehElTaluE9rok=", + "requires": { + "is-regexp": "^1.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "requires": { + "minimist": "^1.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "dev": true, + "requires": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "terser": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.1.tgz", + "integrity": "sha512-pnzH6dnFEsR2aa2SJaKb1uSCl3QmIsJ8dEkj0Fky+2AwMMcC9doMqLOQIH6wVTEKaVfKVvLSk5qxPBEZT9mywg==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "tiny-lr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", + "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", + "requires": { + "body": "^5.1.0", + "debug": "^3.1.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.3.0", + "object-assign": "^4.1.0", + "qs": "^6.4.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "requires": { + "through2": "^2.0.3" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" + }, + "trim-lines": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.2.tgz", + "integrity": "sha512-3GOuyNeTqk3FAqc3jOJtw7FTjYl94XBR5aD9QnDbK/T4CA9sW/J0l9RoaRPE9wyPP7NF331qnHnvJFBJ+IDkmQ==" + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "trim-trailing-lines": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.2.tgz", + "integrity": "sha512-MUjYItdrqqj2zpcHFTkMa9WAv4JHTI6gnRQGPFLrt5L9a6tRMiDnIqYl8JBvu2d2Tc3lWJKQwlGCp0K8AvCM+Q==" + }, + "trough": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.4.tgz", + "integrity": "sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q==" + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, + "try-catch": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz", + "integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg==" + }, + "try-to-catch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz", + "integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "turndown": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-5.0.3.tgz", + "integrity": "sha512-popfGXEiedpq6F5saRIAThKxq/bbEPVFnsDnUdjaDGIre9f3/OL9Yi/yPbPcZ7RYUDpekghr666bBfZPrwNnhQ==", + "requires": { + "jsdom": "^11.9.0" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/type/-/type-1.0.3.tgz", + "integrity": "sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uglify-js": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.6.tgz", + "integrity": "sha512-YDKRX8F0Y+Jr7LhoVk0n4G7ltR3Y7qFAj+DtVBthlOgCcIj1hyMigCfousVfn9HKmvJ+qiFlLDwaHx44/e5ZKw==", + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "undertaker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", + "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + } + }, + "undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "dev": true + }, + "unherit": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.2.tgz", + "integrity": "sha512-W3tMnpaMG7ZY6xe/moK04U9fBhi6wEiCYHUW5Mop/wQHf12+79EQGwxYejNdhEz2mkqkBlGwm7pxmgBKMVUj0w==", + "requires": { + "inherits": "^2.0.1", + "xtend": "^4.0.1" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==" + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==" + }, + "unified": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", + "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "requires": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^2.0.0", + "x-is-string": "^0.1.0" + }, + "dependencies": { + "vfile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", + "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "requires": { + "is-buffer": "^1.1.4", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + } + } + } + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "unist-builder": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-1.0.4.tgz", + "integrity": "sha512-v6xbUPP7ILrT15fHGrNyHc1Xda8H3xVhP7/HAIotHOhVPjH5dCXA097C3Rry1Q2O+HbOLCao4hfPB+EYEjHgVg==", + "requires": { + "object-assign": "^4.1.0" + } + }, + "unist-util-generated": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.4.tgz", + "integrity": "sha512-SA7Sys3h3X4AlVnxHdvN/qYdr4R38HzihoEVY2Q2BZu8NHWDnw5OGcC/tXWjQfd4iG+M6qRFNIRGqJmp2ez4Ww==" + }, + "unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + }, + "unist-util-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.0.3.tgz", + "integrity": "sha512-28EpCBYFvnMeq9y/4w6pbnFmCUfzlsc41NJui5c51hOFjBA1fejcwc+5W4z2+0ECVbScG3dURS3JTVqwenzqZw==" + }, + "unist-util-remove-position": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.3.tgz", + "integrity": "sha512-CtszTlOjP2sBGYc2zcKA/CvNdTdEs3ozbiJ63IPBxh8iZg42SCCb8m04f8z2+V1aSk5a7BxbZKEdoDjadmBkWA==", + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==" + }, + "unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "requires": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "requires": { + "unist-util-is": "^3.0.0" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=" + }, + "vditor": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/vditor/-/vditor-1.8.9.tgz", + "integrity": "sha512-WetgTcj5lDwA+BDkSDnwqZfXYwL4YbAEHLZfU3pFvyhhTagq3Hw4CTO4aZaq0d5q4g37nzADNjL3BpZVuVQpnA==", + "requires": { + "abcjs": "^5.8.0", + "diff-match-patch": "^1.0.4", + "echarts": "^4.2.1", + "highlight.js": "^9.15.9", + "katex": "^0.11.0", + "mermaid": "^8.2.3", + "turndown": "^5.0.3" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vfile": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.0.1.tgz", + "integrity": "sha512-lRHFCuC4SQBFr7Uq91oJDJxlnftoTLQ7eKIpMdubhYcVMho4781a8MWXLy3qZrZ0/STD1kRiKc0cQOHm4OkPeA==", + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + }, + "unist-util-stringify-position": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.1.tgz", + "integrity": "sha512-Zqlf6+FRI39Bah8Q6ZnNGrEHUhwJOkHde2MHVk96lLyftfJJckaPslKgzhVcviXj8KcE9UJM9F+a4JEiBUTYgA==", + "requires": { + "@types/unist": "^2.0.2" + } + }, + "vfile-message": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.1.tgz", + "integrity": "sha512-KtasSV+uVU7RWhUn4Lw+wW1Zl/nW8JWx7JCPps10Y9JRRIDeDXf8wfBLoOSsJLyo27DqMyAi54C6Jf/d6Kr2Bw==", + "requires": { + "@types/unist": "^2.0.2", + "unist-util-stringify-position": "^2.0.0" + } + } + } + }, + "vfile-location": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.5.tgz", + "integrity": "sha512-Pa1ey0OzYBkLPxPZI3d9E+S4BmvfVwNAAXrrqGbwTVXWaX2p9kM1zZ+n35UtVM06shmWKH4RPRN8KI80qE3wNQ==" + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + }, + "vfile-reporter": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-6.0.0.tgz", + "integrity": "sha512-8Is0XxFxWJUhPJdOg3CyZTqd3ICCWg6r304PuBl818ZG91h4FMS3Q+lrOPS+cs5/DZK3H0+AkJdH0J8JEwKtDA==", + "requires": { + "repeat-string": "^1.5.0", + "string-width": "^4.0.0", + "supports-color": "^6.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-sort": "^2.1.2", + "vfile-statistics": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", + "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^5.2.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "unist-util-stringify-position": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.1.tgz", + "integrity": "sha512-Zqlf6+FRI39Bah8Q6ZnNGrEHUhwJOkHde2MHVk96lLyftfJJckaPslKgzhVcviXj8KcE9UJM9F+a4JEiBUTYgA==", + "requires": { + "@types/unist": "^2.0.2" + } + } + } + }, + "vfile-sort": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-2.2.1.tgz", + "integrity": "sha512-5dt7xEhC44h0uRQKhbM2JAe0z/naHphIZlMOygtMBM9Nn0pZdaX5fshhwWit9wvsuP8t/wp43nTDRRErO1WK8g==" + }, + "vfile-statistics": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-1.1.3.tgz", + "integrity": "sha512-CstaK/ebTz1W3Qp41Bt9Lj/2DmumFsCwC2sKahDNSPh0mPh7/UyMLCoU8ZBX34CRU0d61B4W41yIFsV0NKMZeA==" + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + }, + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + } + }, + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "requires": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "dev": true, + "requires": { + "source-map": "^0.5.1" + } + }, + "vue-template-compiler": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz", + "integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==", + "requires": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "zrender": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.1.0.tgz", + "integrity": "sha512-PkWDpBSRIZAKyBBSkizWCP5/bIL5s/+W+FM/AwkivN4m4QCVH04PS0nJmBB/E6CnUNQv9ZPfIV4tthg1qEP1gg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..1b13bf60 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "Solo", + "version": "3.6.5", + "description": " 一款小而美的博客系统,专为程序员设计。", + "homepage": "https://github.com/b3log/solo", + "repository": { + "type": "git", + "url": "git://github.com/b3log/solo.git" + }, + "bugs": { + "url": "https://github.com/b3log/solo/issues" + }, + "license": "AGPLv3", + "private": true, + "scripts": { + "dev": "gulp watch", + "build": "gulp" + }, + "author": "Daniel (http://88250.b3log.org) & Vanessa (http://vanessa.b3log.org)", + "maintainers": [ + { + "name": "Daniel", + "email": "d@b3log.org" + }, + { + "name": "Vanessa", + "email": "v@b3log.org" + } + ], + "devDependencies": { + "del": "^5.1.0", + "gulp": "^4.0.2", + "gulp-concat": "^2.6.1", + "gulp-rename": "^1.4.0", + "gulp-sass": "^4.0.2", + "gulp-uglify": "^3.0.2" + }, + "dependencies": { + "vditor": "^1.8.9" + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..e52dae77 --- /dev/null +++ b/pom.xml @@ -0,0 +1,410 @@ + + + + 4.0.0 + org.b3log + solo + war + Solo + 3.6.5 + + 一款小而美的博客系统,专为程序员设计。 + + 2010 + + + B3log + https://b3log.org + + + + + GNU Affero General Public License, Version 3 + https://www.gnu.org/licenses/agpl-3.0.txt + + + + + + d@b3log.org + Liang Ding + d@b3log.org + http://88250.b3log.org + + Lead + Initial Committer + + B3log + https://b3log.org + +8 + + + + v@b3log.org + Liyuan Li + v@b3log.org + http://vanessa.b3log.org + + Committer + + B3log + https://b3log.org + +8 + + + + + GitHub Issues + https://github.com/b3log/solo/issues + + + + https://github.com/b3log/solo + + + + 2.5.7 + + 1.7.5 + 1.9.1 + 0.50.20 + 9.4.12.v20180830 + 1.3.1 + 1.10 + 4.0.0 + 5.0.3 + 1.2.2 + 1.18 + 2.3.28 + 1.21 + 3.1.0 + + 3.0 + 2.22.1 + 3.2.2 + 4.3.0 + 0.8.2 + 6.1.1 + 8.0.16 + + 1.8 + 1.8 + UTF-8 + + + + + org.b3log + latke-core + ${org.b3log.latke.version} + + + + javax.servlet + javax.servlet-api + ${servlet.version} + + + + mysql + mysql-connector-java + ${mysql-connector-java.version} + + + + org.b3log + latke-repository-mysql + ${org.b3log.latke.version} + + + + org.b3log + latke-repository-h2 + ${org.b3log.latke.version} + + + + com.h2database + h2 + 1.4.199 + + + + org.testng + testng + ${testng.version} + test + + + + org.jsoup + jsoup + ${jsoup.version} + + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + + com.vladsch.flexmark + flexmark-all + ${flexmark.version} + + + com.vladsch.flexmark + flexmark-pdf-converter + + + com.vladsch.flexmark + flexmark-youtrack-converter + + + com.vladsch.flexmark + flexmark-jira-converter + + + + + + com.vdurmont + emoji-java + ${emoji-java.version} + + + org.json + json + + + + + + commons-cli + commons-cli + ${commons-cli.version} + + + + org.eclipse.jetty + jetty-webapp + ${jetty.version} + + + + org.jodd + jodd-http + ${jodd.version} + + + + org.owasp.encoder + encoder + ${owasp.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + commons-codec + commons-codec + ${commons-codec.version} + + + + org.freemarker + freemarker + ${freemarker.version} + + + + eu.bitwalker + UserAgentUtils + ${user-agent-utils.version} + + + + + + + + src/main/resources + + etc/ + lib/ + docker/ + + + + + + + src/main/webapp + + + src/test/resources + + + + + + org.eluder.coveralls + coveralls-maven-plugin + ${coveralls-maven-plugin.version} + + PmmgdTzVuYQAsiOHNZ67bNDuqwDRSKvlm + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + false + + + + + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + +
src/main/resources/etc/header.txt
+ false + true + true + true + + **/src/*/java/**/*.java + **/src/*/webapp/js/*.js + **/src/*/webapp/scss/*.scss + **/src/*/webapp/**/*.ftl + **/src/*/webapp/skins/*/js/*.js + **/src/*/webapp/skins/**/*.scss + **/src/*/webapp/skins/**/*.properties + **/src/*/webapp/plugins/*/js/*.js + **/src/*/webapp/plugins/*/css/*.css + **/src/*/webapp/plugins/*/*.properties + **/src/*/webapp/js/admin/*.js + **/src/*/resources/*.properties + **/src/*/webapp/WEB-INF/*.xml + **/src/test/resources/**/* + **/src/*/resources/docker/* + + + **/src/main/java/**/package-info.java + **/src/*/webapp/js/lib/*.js + **/src/*/webapp/js/*.min.js + **/src/*/webapp/js/admin/*.min.js + **/src/*/webapp/skins/*/js/*.min.js + **/src/*/webapp/skins/*/css/*.css + **/src/main/java/com/**/*.java + + + true + + SLASHSTAR_STYLE + SLASHSTAR_STYLE + + true + + 2010-present + b3log.org + + UTF-8 +
+ + + generate-sources + + format + + + +
+ + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + + 8080 + + stop + 4501 + + / + + + + + + org.apache.maven.plugins + maven-war-plugin + ${maven-war-plugin.version} + + +
+ + solo +
+ + + + ci + + + + dev + + true + + + + aliyun + http://maven.aliyun.com/nexus/content/groups/public + + + + + + aliyun + http://maven.aliyun.com/nexus/content/groups/public + + + + + +
+ diff --git a/scripts/docker-restart.sh b/scripts/docker-restart.sh new file mode 100644 index 00000000..e6e53054 --- /dev/null +++ b/scripts/docker-restart.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# +# Solo docker 更新重启脚本 +# +# 1. 请注意修改参数 +# 2. 可将该脚本加入 crontab,每日凌晨运行来实现自动更新 +# + +docker pull b3log/solo +docker stop solo +docker rm solo +docker run --detach --name solo --network=host \ + --env RUNTIME_DB="MYSQL" \ + --env JDBC_USERNAME="root" \ + --env JDBC_PASSWORD="123456" \ + --env JDBC_DRIVER="com.mysql.cj.jdbc.Driver" \ + --env JDBC_URL="jdbc:mysql://127.0.0.1:3306/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC" \ + b3log/solo --listen_port=8080 --server_scheme=http --server_host=localhost diff --git a/solo.iml b/solo.iml new file mode 100644 index 00000000..1678c6cb --- /dev/null +++ b/solo.iml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/b3log/solo/SoloServletListener.java b/src/main/java/org/b3log/solo/SoloServletListener.java new file mode 100644 index 00000000..3fb4a1cd --- /dev/null +++ b/src/main/java/org/b3log/solo/SoloServletListener.java @@ -0,0 +1,422 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo; + +import eu.bitwalker.useragentutils.BrowserType; +import eu.bitwalker.useragentutils.UserAgent; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.event.EventManager; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.plugin.PluginManager; +import org.b3log.latke.plugin.ViewLoadEventHandler; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.servlet.AbstractServletListener; +import org.b3log.latke.servlet.DispatcherServlet; +import org.b3log.latke.util.Requests; +import org.b3log.latke.util.Stopwatchs; +import org.b3log.latke.util.Strings; +import org.b3log.solo.event.B3ArticleSender; +import org.b3log.solo.event.B3ArticleUpdater; +import org.b3log.solo.event.B3CommentSender; +import org.b3log.solo.event.PluginRefresher; +import org.b3log.solo.model.Option; +import org.b3log.solo.processor.InitCheckHandler; +import org.b3log.solo.processor.PermalinkHandler; +import org.b3log.solo.processor.console.*; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.service.*; +import org.b3log.solo.util.Markdowns; +import org.b3log.solo.util.Skins; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletRequestEvent; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSessionEvent; + +/** + * Solo Servlet listener. + * + * @author Liang Ding + * @author Vanessa + * @version 1.11.0.24, Sep 18, 2019 + * @since 0.3.1 + */ +public final class SoloServletListener extends AbstractServletListener { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SoloServletListener.class); + + /** + * Solo version. + */ + public static final String VERSION = "3.6.5"; + + /** + * Bean manager. + */ + private BeanManager beanManager; + + @Override + public void contextInitialized(final ServletContextEvent servletContextEvent) { + Latkes.setScanPath("org.b3log.solo"); + super.contextInitialized(servletContextEvent); + DispatcherServlet.HANDLERS.add(0, new InitCheckHandler()); + DispatcherServlet.HANDLERS.add(1, new PermalinkHandler()); + + beanManager = BeanManager.getInstance(); + routeConsoleProcessors(); + Stopwatchs.start("Context Initialized"); + + final Latkes.RuntimeDatabase runtimeDatabase = Latkes.getRuntimeDatabase(); + final Latkes.RuntimeMode runtimeMode = Latkes.getRuntimeMode(); + final String jdbcUsername = Latkes.getLocalProperty("jdbc.username"); + final String jdbcURL = Latkes.getLocalProperty("jdbc.URL"); + final boolean luteAvailable = Markdowns.LUTE_AVAILABLE; + + LOGGER.log(Level.INFO, "Solo is booting [ver=" + VERSION + ", servletContainer=" + Latkes.getServletInfo(servletContextEvent.getServletContext()) + + ", os=" + Latkes.getOperatingSystemName() + ", isDocker=" + Latkes.isDocker() + ", luteAvailable=" + luteAvailable + ", pid=" + Latkes.currentPID() + + ", runtimeDatabase=" + runtimeDatabase + ", runtimeMode=" + runtimeMode + ", jdbc.username=" + jdbcUsername + ", jdbc.URL=" + jdbcURL + "]"); + + validateSkin(); + + final InitService initService = beanManager.getReference(InitService.class); + initService.initTables(); + + if (initService.isInited()) { + // Upgrade check https://github.com/b3log/solo/issues/12040 + final UpgradeService upgradeService = beanManager.getReference(UpgradeService.class); + upgradeService.upgrade(); + + // Import check https://github.com/b3log/solo/issues/12293 + final ImportService importService = beanManager.getReference(ImportService.class); + importService.importMarkdowns(); + + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + final Transaction transaction = optionRepository.beginTransaction(); + try { + loadPreference(); + + if (transaction.isActive()) { + transaction.commit(); + } + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + } + } + + registerEventHandlers(); + + final PluginManager pluginManager = beanManager.getReference(PluginManager.class); + pluginManager.load(); + + if (initService.isInited()) { + LOGGER.info("Solo is running"); + } + + Stopwatchs.end(); + LOGGER.log(Level.DEBUG, "Stopwatch: {0}{1}", Strings.LINE_SEPARATOR, Stopwatchs.getTimingStat()); + + final CronMgmtService cronMgmtService = beanManager.getReference(CronMgmtService.class); + cronMgmtService.start(); + } + + @Override + public void contextDestroyed(final ServletContextEvent servletContextEvent) { + super.contextDestroyed(servletContextEvent); + + final CronMgmtService cronMgmtService = beanManager.getReference(CronMgmtService.class); + cronMgmtService.stop(); + + LOGGER.info("Destroyed the context"); + } + + @Override + public void sessionCreated(final HttpSessionEvent httpSessionEvent) { + } + + @Override + public void sessionDestroyed(final HttpSessionEvent httpSessionEvent) { + super.sessionDestroyed(httpSessionEvent); + } + + @Override + public void requestInitialized(final ServletRequestEvent servletRequestEvent) { + final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest(); + Requests.log(httpServletRequest, Level.DEBUG, LOGGER); + + final String requestURI = httpServletRequest.getRequestURI(); + Stopwatchs.start("Request Initialized [requestURI=" + requestURI + "]"); + fillBotAttrs(httpServletRequest); + if (!Solos.isBot(httpServletRequest)) { + final StatisticMgmtService statisticMgmtService = beanManager.getReference(StatisticMgmtService.class); + statisticMgmtService.onlineVisitorCount(httpServletRequest); + } + + resolveSkinDir(httpServletRequest); + } + + @Override + public void requestDestroyed(final ServletRequestEvent servletRequestEvent) { + Stopwatchs.end(); + + LOGGER.log(Level.DEBUG, "Stopwatch: {0}{1}", Strings.LINE_SEPARATOR, Stopwatchs.getTimingStat()); + Stopwatchs.release(); + + super.requestDestroyed(servletRequestEvent); + } + + /** + * Loads skin. + *

+ * Loads skin from repository, loads skins from skin directory then sets it into preference if the skins changed. + *

+ */ + private void loadPreference() { + Stopwatchs.start("Load Preference"); + + LOGGER.debug("Loading preference...."); + + final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class); + JSONObject skin; + try { + skin = optionQueryService.getSkin(); + if (null == skin) { + return; + } + + final SkinMgmtService skinMgmtService = beanManager.getReference(SkinMgmtService.class); + skinMgmtService.loadSkins(skin); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + System.exit(-1); + } + + Stopwatchs.end(); + } + + /** + * Register event handlers. + */ + private void registerEventHandlers() { + Stopwatchs.start("Register Event Handlers"); + LOGGER.debug("Registering event handlers...."); + + try { + final EventManager eventManager = beanManager.getReference(EventManager.class); + final PluginRefresher pluginRefresher = beanManager.getReference(PluginRefresher.class); + eventManager.registerListener(pluginRefresher); + eventManager.registerListener(new ViewLoadEventHandler()); + final B3ArticleSender articleSender = beanManager.getReference(B3ArticleSender.class); + eventManager.registerListener(articleSender); + final B3ArticleUpdater articleUpdater = beanManager.getReference(B3ArticleUpdater.class); + eventManager.registerListener(articleUpdater); + final B3CommentSender commentSender = beanManager.getReference(B3CommentSender.class); + eventManager.registerListener(commentSender); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Register event handlers failed", e); + + System.exit(-1); + } + + LOGGER.debug("Registered event handlers"); + Stopwatchs.end(); + } + + /** + * Resolve skin (template) for the specified HTTP servlet request. + * 前台皮肤切换 https://github.com/b3log/solo/issues/12060 + * + * @param httpServletRequest the specified HTTP servlet request + */ + private void resolveSkinDir(final HttpServletRequest httpServletRequest) { + String skin = Skins.getSkinDirNameFromCookie(httpServletRequest); + if (StringUtils.isBlank(skin)) { + final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class); + final JSONObject skinOpt = optionQueryService.getSkin(); + if (Solos.isMobile(httpServletRequest)) { + if (null != skinOpt) { + skin = skinOpt.optString(Option.ID_C_MOBILE_SKIN_DIR_NAME); + } else { + skin = Option.DefaultPreference.DEFAULT_MOBILE_SKIN_DIR_NAME; + } + } else { + if (null != skinOpt) { + skin = skinOpt.optString(Option.ID_C_SKIN_DIR_NAME); + } else { + skin = Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME; + } + } + } + + httpServletRequest.setAttribute(Keys.TEMAPLTE_DIR_NAME, skin); + } + + private static void fillBotAttrs(final HttpServletRequest request) { + final String userAgentStr = request.getHeader("User-Agent"); + final UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr); + BrowserType browserType = userAgent.getBrowser().getBrowserType(); + + if (StringUtils.containsIgnoreCase(userAgentStr, "mobile") + || StringUtils.containsIgnoreCase(userAgentStr, "MQQBrowser") + || StringUtils.containsIgnoreCase(userAgentStr, "iphone") + || StringUtils.containsIgnoreCase(userAgentStr, "MicroMessenger") + || StringUtils.containsIgnoreCase(userAgentStr, "CFNetwork") + || StringUtils.containsIgnoreCase(userAgentStr, "Android")) { + browserType = BrowserType.MOBILE_BROWSER; + } else if (StringUtils.containsIgnoreCase(userAgentStr, "Iframely") + || StringUtils.containsIgnoreCase(userAgentStr, "Google") + || StringUtils.containsIgnoreCase(userAgentStr, "BUbiNG") + || StringUtils.containsIgnoreCase(userAgentStr, "ltx71") + || StringUtils.containsIgnoreCase(userAgentStr, "py")) { + browserType = BrowserType.ROBOT; + } + + request.setAttribute(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT, BrowserType.ROBOT == browserType); + request.setAttribute(Keys.HttpRequest.IS_MOBILE_BOT, BrowserType.MOBILE_BROWSER == browserType); + } + + /** + * Validates the default skin. + * + *

+ * 改进皮肤加载校验 https://github.com/b3log/solo/issues/12548 + *

+ */ + private static void validateSkin() { + final String skinDirName = Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME; + final String skinName = Latkes.getSkinName(skinDirName); + if (StringUtils.isBlank(skinName)) { + LOGGER.log(Level.ERROR, "Can't load the default skins, please make sure skin [" + skinDirName + "] is under skins directory and structure correctly"); + + System.exit(-1); + } + } + + /** + * 后台控制器使用函数式路由. https://github.com/b3log/solo/issues/12580 + */ + public static void routeConsoleProcessors() { + final BeanManager beanManager = BeanManager.getInstance(); + final AdminConsole adminConsole = beanManager.getReference(AdminConsole.class); + DispatcherServlet.get("/admin-index.do", adminConsole::showAdminIndex); + DispatcherServlet.get("/admin-preference.do", adminConsole::showAdminPreferenceFunction); + DispatcherServlet.route().get(new String[]{"/admin-article.do", + "/admin-article-list.do", + "/admin-comment-list.do", + "/admin-link-list.do", + "/admin-page-list.do", + "/admin-others.do", + "/admin-draft-list.do", + "/admin-user-list.do", + "/admin-category-list.do", + "/admin-theme-list.do", + "/admin-plugin-list.do", + "/admin-main.do", + "/admin-about.do"}, adminConsole::showAdminFunctions); + DispatcherServlet.get("/console/export/sql", adminConsole::exportSQL); + DispatcherServlet.get("/console/export/json", adminConsole::exportJSON); + DispatcherServlet.get("/console/export/hexo", adminConsole::exportHexo); + + final ArticleConsole articleConsole = beanManager.getReference(ArticleConsole.class); + DispatcherServlet.get("/console/article/push2rhy", articleConsole::pushArticleToCommunity); + DispatcherServlet.get("/console/thumbs", articleConsole::getArticleThumbs); + DispatcherServlet.get("/console/article/{id}", articleConsole::getArticle); + DispatcherServlet.get("/console/articles/status/{status}/{page}/{pageSize}/{windowSize}", articleConsole::getArticles); + DispatcherServlet.delete("/console/article/{id}", articleConsole::removeArticle); + DispatcherServlet.put("/console/article/unpublish/{id}", articleConsole::cancelPublishArticle); + DispatcherServlet.put("/console/article/canceltop/{id}", articleConsole::cancelTopArticle); + DispatcherServlet.put("/console/article/puttop/{id}", articleConsole::putTopArticle); + DispatcherServlet.put("/console/article/", articleConsole::updateArticle); + DispatcherServlet.post("/console/article/", articleConsole::addArticle); + + final CategoryConsole categoryConsole = beanManager.getReference(CategoryConsole.class); + DispatcherServlet.put("/console/category/order/", categoryConsole::changeOrder); + DispatcherServlet.get("/console/category/{id}", categoryConsole::getCategory); + DispatcherServlet.delete("/console/category/{id}", categoryConsole::removeCategory); + DispatcherServlet.put("/console/category/", categoryConsole::updateCategory); + DispatcherServlet.post("/console/category/", categoryConsole::addCategory); + DispatcherServlet.get("/console/categories/{page}/{pageSize}/{windowSize}", categoryConsole::getCategories); + + final CommentConsole commentConsole = beanManager.getReference(CommentConsole.class); + DispatcherServlet.delete("/console/article/comment/{id}", commentConsole::removeArticleComment); + DispatcherServlet.get("/console/comments/{page}/{pageSize}/{windowSize}", commentConsole::getComments); + DispatcherServlet.get("/console/comments/article/{id}", commentConsole::getArticleComments); + + final LinkConsole linkConsole = beanManager.getReference(LinkConsole.class); + DispatcherServlet.delete("/console/link/{id}", linkConsole::removeLink); + DispatcherServlet.put("/console/link/", linkConsole::updateLink); + DispatcherServlet.put("/console/link/order/", linkConsole::changeOrder); + DispatcherServlet.post("/console/link/", linkConsole::addLink); + DispatcherServlet.get("/console/links/{page}/{pageSize}/{windowSize}", linkConsole::getLinks); + DispatcherServlet.get("/console/link/{id}", linkConsole::getLink); + + final PageConsole pageConsole = beanManager.getReference(PageConsole.class); + DispatcherServlet.put("/console/page/", pageConsole::updatePage); + DispatcherServlet.delete("/console/page/{id}", pageConsole::removePage); + DispatcherServlet.post("/console/page/", pageConsole::addPage); + DispatcherServlet.put("/console/page/order/", pageConsole::changeOrder); + DispatcherServlet.get("/console/page/{id}", pageConsole::getPage); + DispatcherServlet.get("/console/pages/{page}/{pageSize}/{windowSize}", pageConsole::getPages); + + final PluginConsole pluginConsole = beanManager.getReference(PluginConsole.class); + DispatcherServlet.put("/console/plugin/status/", pluginConsole::setPluginStatus); + DispatcherServlet.get("/console/plugins/{page}/{pageSize}/{windowSize}", pluginConsole::getPlugins); + DispatcherServlet.post("/console/plugin/toSetting", pluginConsole::toSetting); + DispatcherServlet.post("/console/plugin/updateSetting", pluginConsole::updateSetting); + + final PreferenceConsole preferenceConsole = beanManager.getReference(PreferenceConsole.class); + DispatcherServlet.get("/console/signs/", preferenceConsole::getSigns); + DispatcherServlet.get("/console/preference/", preferenceConsole::getPreference); + DispatcherServlet.put("/console/preference/", preferenceConsole::updatePreference); + + final SkinConsole skinConsole = beanManager.getReference(SkinConsole.class); + DispatcherServlet.get("/console/skin", skinConsole::getSkin); + DispatcherServlet.put("/console/skin", skinConsole::updateSkin); + + final RepairConsole repairConsole = beanManager.getReference(RepairConsole.class); + DispatcherServlet.get("/fix/restore-signs", repairConsole::restoreSigns); + + final TagConsole tagConsole = beanManager.getReference(TagConsole.class); + DispatcherServlet.get("/console/tags", tagConsole::getTags); + DispatcherServlet.get("/console/tag/unused", tagConsole::getUnusedTags); + + final OtherConsole otherConsole = beanManager.getReference(OtherConsole.class); + DispatcherServlet.delete("/console/archive/unused", otherConsole::removeUnusedArchives); + DispatcherServlet.delete("/console/tag/unused", otherConsole::removeUnusedTags); + + final UserConsole userConsole = beanManager.getReference(UserConsole.class); + DispatcherServlet.put("/console/user/", userConsole::updateUser); + DispatcherServlet.delete("/console/user/{id}", userConsole::removeUser); + DispatcherServlet.get("/console/users/{page}/{pageSize}/{windowSize}", userConsole::getUsers); + DispatcherServlet.get("/console/user/{id}", userConsole::getUser); + DispatcherServlet.get("/console/changeRole/{id}", userConsole::changeUserRole); + + DispatcherServlet.mapping(); + } +} diff --git a/src/main/java/org/b3log/solo/Starter.java b/src/main/java/org/b3log/solo/Starter.java new file mode 100644 index 00000000..5ca005aa --- /dev/null +++ b/src/main/java/org/b3log/solo/Starter.java @@ -0,0 +1,202 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo; + +import org.apache.commons.cli.*; +import org.b3log.latke.Latkes; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.util.Strings; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Slf4jLog; +import org.eclipse.jetty.webapp.WebAppContext; + +import java.io.File; + +/** + * Solo with embedded Jetty, standalone mode. + *
    + *
  • Windows: java -cp "WEB-INF/lib/*;WEB-INF/classes" org.b3log.solo.Starter
  • + *
  • Unix-like: java -cp "WEB-INF/lib/*:WEB-INF/classes" org.b3log.solo.Starter
  • + *
+ * + * @author Liang Ding + * @version 1.1.1.0, Mar 29, 2019 + * @since 1.2.0 + */ +public final class Starter { + + static { + try { + Log.setLog(new Slf4jLog()); + } catch (final Exception e) { + e.printStackTrace(); + } + } + + /** + * Main. + * + * @param args the specified arguments + * @throws java.lang.Exception if start failed + */ + public static void main(final String[] args) throws Exception { + final Logger logger = Logger.getLogger(Starter.class); + + final Options options = new Options(); + final Option listenPortOpt = Option.builder("lp").longOpt("listen_port").argName("LISTEN_PORT"). + hasArg().desc("listen port, default is 8080").build(); + options.addOption(listenPortOpt); + + final Option serverSchemeOpt = Option.builder("ss").longOpt("server_scheme").argName("SERVER_SCHEME"). + hasArg().desc("browser visit protocol, default is http").build(); + options.addOption(serverSchemeOpt); + + final Option serverHostOpt = Option.builder("sh").longOpt("server_host").argName("SERVER_HOST"). + hasArg().desc("browser visit domain name, default is localhost").build(); + options.addOption(serverHostOpt); + + final Option serverPortOpt = Option.builder("sp").longOpt("server_port").argName("SERVER_PORT"). + hasArg().desc("browser visit port, default is 8080").build(); + options.addOption(serverPortOpt); + + final Option staticServerSchemeOpt = Option.builder("sss").longOpt("static_server_scheme").argName("STATIC_SERVER_SCHEME"). + hasArg().desc("browser visit static resource protocol, default is http").build(); + options.addOption(staticServerSchemeOpt); + + final Option staticServerHostOpt = Option.builder("ssh").longOpt("static_server_host").argName("STATIC_SERVER_HOST"). + hasArg().desc("browser visit static resource domain name, default is localhost").build(); + options.addOption(staticServerHostOpt); + + final Option staticServerPortOpt = Option.builder("ssp").longOpt("static_server_port").argName("STATIC_SERVER_PORT"). + hasArg().desc("browser visit static resource port, default is 8080").build(); + options.addOption(staticServerPortOpt); + + final Option runtimeModeOpt = Option.builder("rm").longOpt("runtime_mode").argName("RUNTIME_MODE"). + hasArg().desc("runtime mode (DEVELOPMENT/PRODUCTION), default is DEVELOPMENT").build(); + options.addOption(runtimeModeOpt); + + options.addOption("h", "help", false, "print help for the command"); + + final HelpFormatter helpFormatter = new HelpFormatter(); + final CommandLineParser commandLineParser = new DefaultParser(); + CommandLine commandLine; + + final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows"); + final String cmdSyntax = isWindows ? "java -cp \"WEB-INF/lib/*;WEB-INF/classes\" org.b3log.solo.Starter" + : "java -cp \"WEB-INF/lib/*:WEB-INF/classes\" org.b3log.solo.Starter"; + final String header = "\nSolo 是一款小而美的博客系统,专为程序员设计。\n\n"; + final String footer = "\n提需求或报告缺陷请到项目网站: https://github.com/b3log/solo\n\n"; + try { + commandLine = commandLineParser.parse(options, args); + } catch (final ParseException e) { + helpFormatter.printHelp(cmdSyntax, header, options, footer, true); + + return; + } + + if (commandLine.hasOption("h")) { + helpFormatter.printHelp(cmdSyntax, header, options, footer, true); + + return; + } + + String portArg = commandLine.getOptionValue("listen_port"); + if (!Strings.isNumeric(portArg)) { + portArg = "8080"; + } + + try { + Latkes.init(); + } catch (final Exception e) { + logger.log(Level.ERROR, "Latke init failed, please configure latke.props or run with args, visit https://hacpai.com/article/1492881378588 for more details"); + + System.exit(-1); + } + + String serverScheme = commandLine.getOptionValue("server_scheme"); + if (null != serverScheme) { + Latkes.setLatkeProperty("serverScheme", serverScheme); + } + String serverHost = commandLine.getOptionValue("server_host"); + if (null != serverHost) { + Latkes.setLatkeProperty("serverHost", serverHost); + } + String serverPort = commandLine.getOptionValue("server_port"); + if (null != serverPort) { + Latkes.setLatkeProperty("serverPort", serverPort); + } + String staticServerScheme = commandLine.getOptionValue("static_server_scheme"); + if (null != staticServerScheme) { + Latkes.setLatkeProperty("staticServerScheme", staticServerScheme); + } + String staticServerHost = commandLine.getOptionValue("static_server_host"); + if (null != staticServerHost) { + Latkes.setLatkeProperty("staticServerHost", staticServerHost); + } + String staticServerPort = commandLine.getOptionValue("static_server_port"); + if (null != staticServerPort) { + Latkes.setLatkeProperty("staticServerPort", staticServerPort); + } + String runtimeMode = commandLine.getOptionValue("runtime_mode"); + if (null != runtimeMode) { + Latkes.setRuntimeMode(Latkes.RuntimeMode.valueOf(runtimeMode)); + } + + String webappDirLocation = "src/main/webapp/"; // POM structure in dev env + final File file = new File(webappDirLocation); + if (!file.exists()) { + webappDirLocation = "."; // production environment + } + + final int port = Integer.valueOf(portArg); + final Server server = new Server(port); + final WebAppContext root = new WebAppContext(); + root.setParentLoaderPriority(true); // Use parent class loader + root.setContextPath("/"); + root.setDescriptor(webappDirLocation + "/WEB-INF/web.xml"); + root.setResourceBase(webappDirLocation); + server.setHandler(root); + try { + server.start(); + } catch (final Exception e) { + logger.log(Level.ERROR, "Server start failed", e); + + System.exit(-1); + } + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + server.stop(); + } catch (final Exception e) { + logger.log(Level.ERROR, "Server stop failed", e); + + System.exit(-1); + } + })); + + server.join(); + } + + /** + * Private constructor. + */ + private Starter() { + } +} diff --git a/src/main/java/org/b3log/solo/cache/ArticleCache.java b/src/main/java/org/b3log/solo/cache/ArticleCache.java new file mode 100644 index 00000000..e9aa0b74 --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/ArticleCache.java @@ -0,0 +1,105 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.cache; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.solo.model.Article; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Article cache. + * + * @author Liang Ding + * @version 1.2.0.1, Sep 25, 2018 + * @since 2.3.0 + */ +@Singleton +public class ArticleCache { + + /** + * Article id cache. + */ + private final Map idCache = new ConcurrentHashMap<>(); + + /** + * Article permalink cache. + */ + private final Map permalinkCache = new ConcurrentHashMap<>(); + + /** + * Gets an article by the specified article id. + * + * @param id the specified article id + * @return article, returns {@code null} if not found + */ + public JSONObject getArticle(final String id) { + final JSONObject article = idCache.get(id); + if (null == article) { + return null; + } + + return Solos.clone(article); + } + + /** + * Gets an article by the specified article permalink. + * + * @param permalink the specified article permalink + * @return article, returns {@code null} if not found + */ + public JSONObject getArticleByPermalink(final String permalink) { + final JSONObject article = permalinkCache.get(permalink); + if (null == article) { + return null; + } + + return Solos.clone(article); + } + + /** + * Adds or updates the specified article. + * + * @param article the specified article + */ + public void putArticle(final JSONObject article) { + idCache.put(article.optString(Keys.OBJECT_ID), Solos.clone(article)); + permalinkCache.put(article.optString(Article.ARTICLE_PERMALINK), Solos.clone(article)); + } + + /** + * Removes an article by the specified article id. + * + * @param id the specified article id + */ + public void removeArticle(final String id) { + idCache.remove(id); + } + + /** + * Clears all cached data. + */ + public void clear() { + idCache.clear(); + permalinkCache.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/org/b3log/solo/cache/CommentCache.java b/src/main/java/org/b3log/solo/cache/CommentCache.java new file mode 100644 index 00000000..148ec48d --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/CommentCache.java @@ -0,0 +1,82 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.cache; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Comment cache. + * + * @author Liang Ding + * @version 1.1.0.1, Sep 25, 2018 + * @since 2.3.0 + */ +@Singleton +public class CommentCache { + + /** + * Comment cache. + */ + private final Map cache = new ConcurrentHashMap<>(); + + /** + * Gets a comment by the specified comment id. + * + * @param id the specified comment id + * @return comment, returns {@code null} if not found + */ + public JSONObject getComment(final String id) { + final JSONObject comment = cache.get(id); + if (null == comment) { + return null; + } + + return Solos.clone(comment); + } + + /** + * Adds or updates the specified comment. + * + * @param comment the specified comment + */ + public void putComment(final JSONObject comment) { + cache.put(comment.optString(Keys.OBJECT_ID), Solos.clone(comment)); + } + + /** + * Removes a comment by the specified comment id. + * + * @param id the specified comment id + */ + public void removeComment(final String id) { + cache.remove(id); + } + + /** + * Clears all cached data. + */ + public void clear() { + cache.clear(); + } +} diff --git a/src/main/java/org/b3log/solo/cache/OptionCache.java b/src/main/java/org/b3log/solo/cache/OptionCache.java new file mode 100644 index 00000000..44ae474c --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/OptionCache.java @@ -0,0 +1,134 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.cache; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.solo.model.Option; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Option cache. + * + * @author Liang Ding + * @version 1.1.0.1, Sep 25, 2018 + * @since 2.3.0 + */ +@Singleton +public class OptionCache { + + /** + * Option cache. + */ + private final Map cache = new ConcurrentHashMap<>(); + + /** + * Category option caches. + */ + private final Map categoryCache = new ConcurrentHashMap<>(); + + /** + * Removes a category cache specified by the given category. + * + * @param category the given category + */ + public void removeCategory(final String category) { + categoryCache.remove(category); + } + + /** + * Gets merged options as a JSON object for the specified category + * + * @param category the specified category + * @return merged options + */ + public JSONObject getCategory(final String category) { + JSONObject ret = categoryCache.get(category); + if (null == ret) { + return null; + } + + return Solos.clone(ret); + } + + /** + * Puts the specified merged options with the specified category. + * + * @param category the specified category + * @param mergedOptions the specified merged options + */ + public void putCategory(final String category, final JSONObject mergedOptions) { + categoryCache.put(category, mergedOptions); + } + + /** + * Gets an option by the specified option id. + * + * @param id the specified option id + * @return option, returns {@code null} if not found + */ + public JSONObject getOption(final String id) { + final JSONObject option = cache.get(id); + if (null == option) { + return null; + } + + return Solos.clone(option); + } + + /** + * Adds or updates the specified option. + * + * @param option the specified option + */ + public void putOption(final JSONObject option) { + cache.put(option.optString(Keys.OBJECT_ID), Solos.clone(option)); + + final String category = option.optString(Option.OPTION_CATEGORY); + removeCategory(category); + } + + /** + * Removes an option by the specified option id. + * + * @param id the specified option id + */ + public void removeOption(final String id) { + final JSONObject option = getOption(id); + if (null == option) { + return; + } + + final String category = option.optString(Option.OPTION_CATEGORY); + removeCategory(category); + + cache.remove(id); + } + + /** + * Clears all cached data. + */ + public void clear() { + cache.clear(); + categoryCache.clear(); + } +} diff --git a/src/main/java/org/b3log/solo/cache/PageCache.java b/src/main/java/org/b3log/solo/cache/PageCache.java new file mode 100644 index 00000000..252380e3 --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/PageCache.java @@ -0,0 +1,84 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.cache; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Page cache. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 25, 2018 + * @since 2.3.0 + */ +@Singleton +public class PageCache { + + /** + * Page cache. + */ + private final Map cache = new ConcurrentHashMap<>(); + + /** + * Gets a page by the specified page id. + * + * @param id the specified page id + * @return page, returns {@code null} if not found + */ + public JSONObject getPage(final String id) { + final JSONObject page = cache.get(id); + if (null == page) { + return null; + } + + return Solos.clone(page); + } + + /** + * Adds or updates the specified page. + * + * @param page the specified page + */ + public void putPage(final JSONObject page) { + final String pageId = page.optString(Keys.OBJECT_ID); + + cache.put(pageId, Solos.clone(page)); + } + + /** + * Removes an page by the specified page id. + * + * @param id the specified page id + */ + public void removePage(final String id) { + cache.remove(id); + } + + /** + * Clears all cached data. + */ + public void clear() { + cache.clear(); + } +} diff --git a/src/main/java/org/b3log/solo/cache/StatisticCache.java b/src/main/java/org/b3log/solo/cache/StatisticCache.java new file mode 100644 index 00000000..febbfb99 --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/StatisticCache.java @@ -0,0 +1,66 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.cache; + +import org.b3log.latke.ioc.Singleton; +import org.b3log.solo.model.Option; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Statistic cache. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 25, 2018 + * @since 2.4.0 + */ +@Singleton +public class StatisticCache { + + /** + * Statistic cache. + */ + private final Map cache = new ConcurrentHashMap<>(); + + /** + * Get the statistic. + * + * @return statistic + */ + public JSONObject getStatistic() { + return cache.get(Option.CATEGORY_C_STATISTIC); + } + + /** + * Adds or updates the specified statistic. + * + * @param statistic the specified statistic + */ + public void putStatistic(final JSONObject statistic) { + cache.put(Option.CATEGORY_C_STATISTIC, statistic); + } + + /** + * Clears all cached data. + */ + public void clear() { + cache.clear(); + } +} diff --git a/src/main/java/org/b3log/solo/cache/UserCache.java b/src/main/java/org/b3log/solo/cache/UserCache.java new file mode 100644 index 00000000..31fe8363 --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/UserCache.java @@ -0,0 +1,112 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.cache; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.model.Role; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * User cache. + * + * @author Liang Ding + * @version 1.1.0.2, Mar 3, 2019 + * @since 2.3.0 + */ +@Singleton +public class UserCache { + + /** + * Id, User. + */ + private final Map idCache = new ConcurrentHashMap<>(); + + /** + * Admin user. + */ + private final Map adminCache = new ConcurrentHashMap<>(); + + /** + * Gets the admin user. + * + * @return admin user + */ + public JSONObject getAdmin() { + return adminCache.get(Role.ADMIN_ROLE); + } + + /** + * Adds or updates the admin user. + * + * @param admin the specified admin user + */ + public void putAdmin(final JSONObject admin) { + adminCache.put(Role.ADMIN_ROLE, admin); + } + + /** + * Gets a user by the specified user id. + * + * @param userId the specified user id + * @return user, returns {@code null} if not found + */ + public JSONObject getUser(final String userId) { + final JSONObject user = idCache.get(userId); + if (null == user) { + return null; + } + + return Solos.clone(user); + } + + /** + * Adds or updates the specified user. + * + * @param user the specified user + */ + public void putUser(final JSONObject user) { + idCache.put(user.optString(Keys.OBJECT_ID), Solos.clone(user)); + } + + /** + * Removes a user by the specified user id. + * + * @param id the specified user id + */ + public void removeUser(final String id) { + final JSONObject user = idCache.get(id); + if (null == user) { + return; + } + + idCache.remove(id); + } + + /** + * Clears all cached data. + */ + public void clear() { + idCache.clear(); + adminCache.clear(); + } +} diff --git a/src/main/java/org/b3log/solo/cache/package-info.java b/src/main/java/org/b3log/solo/cache/package-info.java new file mode 100644 index 00000000..c9794e08 --- /dev/null +++ b/src/main/java/org/b3log/solo/cache/package-info.java @@ -0,0 +1,4 @@ +/** + * Cache. + */ +package org.b3log.solo.cache; diff --git a/src/main/java/org/b3log/solo/event/B3ArticleSender.java b/src/main/java/org/b3log/solo/event/B3ArticleSender.java new file mode 100644 index 00000000..2941dedd --- /dev/null +++ b/src/main/java/org/b3log/solo/event/B3ArticleSender.java @@ -0,0 +1,145 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.event; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.event.AbstractEventListener; +import org.b3log.latke.event.Event; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.User; +import org.b3log.latke.util.Strings; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.service.ArticleQueryService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +/** + * This listener is responsible for sending article to B3log Rhythm. Sees B3log 构思 for more details. + *

+ * API spec: https://hacpai.com/article/1457158841475 + *

+ * + * @author Liang Ding + * @author ArmstrongCN + * @version 1.0.2.20, Apr 13, 2019 + * @since 0.3.1 + */ +@Singleton +public class B3ArticleSender extends AbstractEventListener { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(B3ArticleSender.class); + + @Override + public void action(final Event event) { + final JSONObject data = event.getData(); + LOGGER.log(Level.DEBUG, "Processing an event [type={0}, data={1}] in listener [className={2}]", + event.getType(), data, B3ArticleSender.class.getName()); + + pushArticleToRhy(data); + } + + /** + * Pushes the specified article data to B3log Rhythm. + * + * @param data the specified article data + */ + public static void pushArticleToRhy(final JSONObject data) { + try { + final JSONObject originalArticle = data.getJSONObject(Article.ARTICLE); + final String title = originalArticle.getString(Article.ARTICLE_TITLE); + if (Article.ARTICLE_STATUS_C_PUBLISHED != originalArticle.optInt(Article.ARTICLE_STATUS)) { + LOGGER.log(Level.INFO, "Ignored push a draft [title={0}] to Rhy", title); + + return; + } + + if (StringUtils.isNotBlank(originalArticle.optString(Article.ARTICLE_VIEW_PWD))) { + LOGGER.log(Level.INFO, "Article [title={0}] is a password article, ignored push to Rhy", title); + + return; + } + + if (!originalArticle.optBoolean(Common.POST_TO_COMMUNITY)) { + LOGGER.log(Level.INFO, "Article [title={0}] push flag [postToCommunity] is false, ignored push to Rhy", title); + + return; + } + + if (StringUtils.containsIgnoreCase(Latkes.getServePath(), ("localhost")) || Strings.isIPv4(Latkes.getServerHost())) { + LOGGER.log(Level.INFO, "Solo is running on local server, ignored push article [title={0}] to Rhy", title); + + return; + } + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class); + final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class); + final JSONObject preference = optionQueryService.getPreference(); + + final JSONObject article = new JSONObject(). + put("id", originalArticle.getString(Keys.OBJECT_ID)). + put("title", originalArticle.getString(Article.ARTICLE_TITLE)). + put("permalink", originalArticle.getString(Article.ARTICLE_PERMALINK)). + put("tags", originalArticle.getString(Article.ARTICLE_TAGS_REF)). + put("content", originalArticle.getString(Article.ARTICLE_CONTENT)); + final JSONObject author = articleQueryService.getAuthor(originalArticle); + final JSONObject client = new JSONObject(). + put("title", preference.getString(Option.ID_C_BLOG_TITLE)). + put("host", Latkes.getServePath()). + put("name", "Solo"). + put("ver", SoloServletListener.VERSION). + put("userName", author.optString(User.USER_NAME)). + put("userB3Key", author.optString(UserExt.USER_B3_KEY)); + final JSONObject requestJSONObject = new JSONObject(). + put("article", article). + put("client", client); + final HttpResponse response = HttpRequest.post("https://rhythm.b3log.org/api/article").bodyText(requestJSONObject.toString()). + connectionTimeout(3000).timeout(7000).trustAllCerts(true). + contentTypeJson().header("User-Agent", Solos.USER_AGENT).send(); + + LOGGER.log(Level.INFO, "Pushed an article [title={0}] to Rhy, response [{1}]", title, response.toString()); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Pushes an article to Rhy failed: " + e.getMessage()); + } + } + + /** + * Gets the event type {@linkplain EventTypes#ADD_ARTICLE}. + * + * @return event type + */ + @Override + public String getEventType() { + return EventTypes.ADD_ARTICLE; + } +} diff --git a/src/main/java/org/b3log/solo/event/B3ArticleUpdater.java b/src/main/java/org/b3log/solo/event/B3ArticleUpdater.java new file mode 100644 index 00000000..49fd87ff --- /dev/null +++ b/src/main/java/org/b3log/solo/event/B3ArticleUpdater.java @@ -0,0 +1,62 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.event; + +import org.b3log.latke.event.AbstractEventListener; +import org.b3log.latke.event.Event; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.json.JSONObject; + +/** + * This listener is responsible for updating article to B3log Rhythm. Sees B3log 构思 for more details. + *

+ * API spec: https://hacpai.com/article/1457158841475 + *

+ * + * @author Liang Ding + * @version 1.0.1.6, Feb 8, 2019 + * @since 0.6.0 + */ +@Singleton +public class B3ArticleUpdater extends AbstractEventListener { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(B3ArticleUpdater.class); + + public void action(final Event event) { + final JSONObject data = event.getData(); + LOGGER.log(Level.DEBUG, "Processing an event [type={0}, data={1}] in listener [className={2}]", + event.getType(), data, B3ArticleUpdater.class.getName()); + + B3ArticleSender.pushArticleToRhy(data); + } + + /** + * Gets the event type {@linkplain EventTypes#UPDATE_ARTICLE}. + * + * @return event type + */ + @Override + public String getEventType() { + return EventTypes.UPDATE_ARTICLE; + } +} diff --git a/src/main/java/org/b3log/solo/event/B3CommentSender.java b/src/main/java/org/b3log/solo/event/B3CommentSender.java new file mode 100644 index 00000000..b9a56706 --- /dev/null +++ b/src/main/java/org/b3log/solo/event/B3CommentSender.java @@ -0,0 +1,136 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.event; + +import jodd.http.HttpRequest; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.event.AbstractEventListener; +import org.b3log.latke.event.Event; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.User; +import org.b3log.latke.util.Strings; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Comment; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.UserRepository; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +/** + * This listener is responsible for sending comment to B3log Rhythm. Sees B3log 构思 for more details. + * + * @author Liang Ding + * @version 1.0.1.6, Mar 27, 2019 + * @since 0.5.5 + */ +@Singleton +public class B3CommentSender extends AbstractEventListener { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(B3CommentSender.class); + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + @Override + public void action(final Event event) { + final JSONObject data = event.getData(); + + LOGGER.log(Level.DEBUG, "Processing an event [type={0}, data={1}] in listener [className={2}]", + event.getType(), data, B3ArticleSender.class.getName()); + try { + final JSONObject originalComment = data.getJSONObject(Comment.COMMENT); + + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + LOGGER.log(Level.ERROR, "Not found preference"); + + return; + } + + if (Latkes.getServePath().contains("localhost") || Strings.isIPv4(Latkes.getServerHost())) { + LOGGER.log(Level.TRACE, "Solo runs on local server, so should not send this comment[id={0}] to Symphony", + originalComment.getString(Keys.OBJECT_ID)); + return; + } + + final String articleId = originalComment.getString(Comment.COMMENT_ON_ID); + final JSONObject article = articleRepository.get(articleId); + final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID); + final JSONObject articleAuthor = userRepository.get(articleAuthorId); + + final JSONObject comment = new JSONObject(). + put("id", originalComment.optString(Keys.OBJECT_ID)). + put("articleId", articleId). + put("content", originalComment.getString(Comment.COMMENT_CONTENT)). + put("authorName", originalComment.optString(Comment.COMMENT_NAME)); + final JSONObject client = new JSONObject(). + put("title", preference.getString(Option.ID_C_BLOG_TITLE)). + put("host", Latkes.getServePath()). + put("name", "Solo"). + put("ver", SoloServletListener.VERSION). + put("userName", articleAuthor.optString(User.USER_NAME)). + put("userB3Key", articleAuthor.optString(UserExt.USER_B3_KEY)); + final JSONObject requestJSONObject = new JSONObject(). + put("comment", comment). + put("client", client); + + HttpRequest.post("https://rhythm.b3log.org/api/comment").bodyText(requestJSONObject.toString()). + connectionTimeout(3000).timeout(7000).trustAllCerts(true). + header("User-Agent", Solos.USER_AGENT).contentTypeJson().sendAsync(); + LOGGER.log(Level.DEBUG, "Pushed a comment to Sym"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Pushes a comment to Sym failed: " + e.getMessage()); + } + } + + /** + * Gets the event type {@linkplain EventTypes#ADD_COMMENT_TO_ARTICLE}. + * + * @return event type + */ + @Override + public String getEventType() { + return EventTypes.ADD_COMMENT_TO_ARTICLE; + } +} diff --git a/src/main/java/org/b3log/solo/event/EventTypes.java b/src/main/java/org/b3log/solo/event/EventTypes.java new file mode 100644 index 00000000..1ab38549 --- /dev/null +++ b/src/main/java/org/b3log/solo/event/EventTypes.java @@ -0,0 +1,59 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.event; + +/** + * Event types. + * + * @author Liang Ding + * @version 1.1.0.8, Feb 10, 2019 + * @since 0.3.1 + */ +public final class EventTypes { + + /** + * Indicates a add article event. + */ + public static final String ADD_ARTICLE = "Add Article"; + + /** + * Indicates a update article event. + */ + public static final String UPDATE_ARTICLE = "Update Article"; + + /** + * Indicates a before render article event. + */ + public static final String BEFORE_RENDER_ARTICLE = "Before Render Article"; + + /** + * Indicates an add comment to article event. + */ + public static final String ADD_COMMENT_TO_ARTICLE = "Add Comment To Article"; + + /** + * Indicates an add comment to page event. + */ + public static final String ADD_COMMENT_TO_PAGE = "Add Comment To Page"; + + /** + * Private constructor. + */ + private EventTypes() { + } +} diff --git a/src/main/java/org/b3log/solo/event/PluginRefresher.java b/src/main/java/org/b3log/solo/event/PluginRefresher.java new file mode 100644 index 00000000..f594ffc4 --- /dev/null +++ b/src/main/java/org/b3log/solo/event/PluginRefresher.java @@ -0,0 +1,82 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.event; + +import org.b3log.latke.event.AbstractEventListener; +import org.b3log.latke.event.Event; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.plugin.AbstractPlugin; +import org.b3log.latke.plugin.PluginManager; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.repository.PluginRepository; +import org.b3log.solo.service.PluginMgmtService; + +import java.util.List; + +/** + * This listener is responsible for refreshing plugin after every loaded. + * + * @author Liang Ding + * @version 1.0.0.2, Sep 25, 2018 + * @since 0.3.1 + */ +@Singleton +public class PluginRefresher extends AbstractEventListener> { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PluginRefresher.class); + + @Override + public void action(final Event> event) { + final List plugins = event.getData(); + + LOGGER.log(Level.DEBUG, "Processing an event [type={0}, data={1}] in listener [className={2}]", + event.getType(), plugins, PluginRefresher.class.getName()); + + final BeanManager beanManager = BeanManager.getInstance(); + final PluginRepository pluginRepository = beanManager.getReference(PluginRepository.class); + + final Transaction transaction = pluginRepository.beginTransaction(); + try { + final PluginMgmtService pluginMgmtService = beanManager.getReference(PluginMgmtService.class); + pluginMgmtService.refresh(plugins); + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Process plugin loaded event error", e); + } + } + + /** + * Gets the event type {@linkplain PluginManager#PLUGIN_LOADED_EVENT}. + * + * @return event type + */ + @Override + public String getEventType() { + return PluginManager.PLUGIN_LOADED_EVENT; + } +} diff --git a/src/main/java/org/b3log/solo/event/package-info.java b/src/main/java/org/b3log/solo/event/package-info.java new file mode 100644 index 00000000..7d0ed51f --- /dev/null +++ b/src/main/java/org/b3log/solo/event/package-info.java @@ -0,0 +1,4 @@ +/** + * Event handlers. + */ +package org.b3log.solo.event; diff --git a/src/main/java/org/b3log/solo/model/ArchiveDate.java b/src/main/java/org/b3log/solo/model/ArchiveDate.java new file mode 100644 index 00000000..f8ad2c55 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/ArchiveDate.java @@ -0,0 +1,64 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +/** + * This class defines all archive date model relevant keys. + * + * @author Liang Ding + * @version 1.0.0.5, Jan 28, 2019 + */ +public final class ArchiveDate { + + /** + * Archive date. + */ + public static final String ARCHIVE_DATE = "archiveDate"; + + /** + * Archive dates. + */ + public static final String ARCHIVE_DATES = "archiveDates"; + + /** + * Archive time. + */ + public static final String ARCHIVE_TIME = "archiveTime"; + + //// Transient //// + /** + * Key of archive date article count. + */ + public static final String ARCHIVE_DATE_T_PUBLISHED_ARTICLE_COUNT = "archiveDatePublishedArticleCount"; + + /** + * Archive date year. + */ + public static final String ARCHIVE_DATE_YEAR = "archiveDateYear"; + + /** + * Archive date month. + */ + public static final String ARCHIVE_DATE_MONTH = "archiveDateMonth"; + + /** + * Private constructor. + */ + private ArchiveDate() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Article.java b/src/main/java/org/b3log/solo/model/Article.java new file mode 100644 index 00000000..9c16da8c --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Article.java @@ -0,0 +1,268 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.util.Images; +import org.b3log.solo.util.Markdowns; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; + +/** + * This class defines all article model relevant keys. + * + * @author Liang Ding + * @version 1.5.0.2, Sep 12, 2019 + * @since 0.3.1 + */ +public final class Article { + + /** + * Article. + */ + public static final String ARTICLE = "article"; + + /** + * Articles. + */ + public static final String ARTICLES = "articles"; + + /** + * Key of title. + */ + public static final String ARTICLE_TITLE = "articleTitle"; + + /** + * Key of abstract. + */ + public static final String ARTICLE_ABSTRACT = "articleAbstract"; + + /** + * Key of abstract text. + */ + public static final String ARTICLE_ABSTRACT_TEXT = "articleAbstractText"; + + /** + * Key of content. + */ + public static final String ARTICLE_CONTENT = "articleContent"; + + /** + * Key of created at. + */ + public static final String ARTICLE_CREATED = "articleCreated"; + + /** + * Key of create date. + */ + public static final String ARTICLE_T_CREATE_DATE = "articleCreateDate"; + + /** + * Key of create time. + */ + public static final String ARTICLE_CREATE_TIME = "articleCreateTime"; + + /** + * Key of updated at. + */ + public static final String ARTICLE_UPDATED = "articleUpdated"; + + /** + * Key of update date. + */ + public static final String ARTICLE_T_UPDATE_DATE = "articleUpdateDate"; + + /** + * Key of update time. + */ + public static final String ARTICLE_UPDATE_TIME = "articleUpdateTime"; + + /** + * Key of tags. + */ + public static final String ARTICLE_TAGS_REF = "articleTags"; + + /** + * Key of comment count. + */ + public static final String ARTICLE_COMMENT_COUNT = "articleCommentCount"; + + /** + * Key of view count. + */ + public static final String ARTICLE_VIEW_COUNT = "articleViewCount"; + + /** + * Key of comments. + */ + public static final String ARTICLE_COMMENTS_REF = "articleComments"; + + /** + * Key of sign id. + */ + public static final String ARTICLE_SIGN_ID = "articleSignId"; + + /** + * Key of permalink. + */ + public static final String ARTICLE_PERMALINK = "articlePermalink"; + + /** + * Key of put top. + */ + public static final String ARTICLE_PUT_TOP = "articlePutTop"; + + /** + * Key of author id. + */ + public static final String ARTICLE_AUTHOR_ID = "articleAuthorId"; + + /** + * Key of random double. + */ + public static final String ARTICLE_RANDOM_DOUBLE = "articleRandomDouble"; + + /** + * Key of comment-able. + */ + public static final String ARTICLE_COMMENTABLE = "articleCommentable"; + + /** + * Key of view password. + */ + public static final String ARTICLE_VIEW_PWD = "articleViewPwd"; + + /** + * Key of article image1 URL. https://github.com/b3log/solo/issues/12670 + */ + public static final String ARTICLE_IMG1_URL = "articleImg1URL"; + + /** + * Key of article status. + */ + public static final String ARTICLE_STATUS = "articleStatus"; + + //// Status constants + + /** + * Article status - published. + */ + public static final int ARTICLE_STATUS_C_PUBLISHED = 0; + + /** + * Article status - draft. + */ + public static final int ARTICLE_STATUS_C_DRAFT = 1; + + //// Transient //// + /** + * Key of article ToC. + */ + public static final String ARTICLE_T_TOC = "articleToC"; + + //// Other constants + + /** + * Article abstract length. + */ + private static final int ARTICLE_ABSTRACT_LENGTH = 500; + + /** + * Width of article first image. + */ + public static final int ARTICLE_THUMB_IMG_WIDTH = 1280; + + /** + * Height of article first image. + */ + public static final int ARTICLE_THUMB_IMG_HEIGHT = 720; + + /** + * Private constructor. + */ + private Article() { + } + + /** + * Gets the first image URL of the specified article. + * + * @param article the specified article + * @return the first image URL, returns {@code ""} if not found + */ + public static String getArticleImg1URL(final JSONObject article) { + final String summary = article.optString(Article.ARTICLE_ABSTRACT); + String content = article.optString(Article.ARTICLE_CONTENT); + content = summary + "\n\n" + content; + final String html = Markdowns.toHTML(content); + final String[] imgs = StringUtils.substringsBetween(html, ""); + if (null == imgs || 0 == imgs.length) { + return Images.imageSize(Images.randImage(), ARTICLE_THUMB_IMG_WIDTH, ARTICLE_THUMB_IMG_HEIGHT); + } + + String ret = null; + for (final String img : imgs) { + ret = StringUtils.substringBetween(img, "src=\"", "\""); + if (!StringUtils.containsIgnoreCase(ret, ".ico")) { + break; + } + } + + if (StringUtils.isBlank(ret)) { + return Images.imageSize(Images.randImage(), ARTICLE_THUMB_IMG_WIDTH, ARTICLE_THUMB_IMG_HEIGHT); + } + + ret = Images.imageSize(ret, ARTICLE_THUMB_IMG_WIDTH, ARTICLE_THUMB_IMG_HEIGHT); + + return ret; + } + + /** + * Gets the abstract plain text of the specified content. + * + * @param content the specified content + * @return the abstract plain text + */ + public static String getAbstractText(final String content) { + final String ret = Jsoup.clean(Markdowns.toHTML(content), Whitelist.none()); + if (ret.length() > ARTICLE_ABSTRACT_LENGTH) { + return ret.substring(0, ARTICLE_ABSTRACT_LENGTH) + "...."; + } + + return ret; + } + + /** + * Gets the abstract plain text of the specified article. + * + * @param article the specified article + * @return the abstract plain text + */ + public static String getAbstractText(final JSONObject article) { + String content = article.optString(Article.ARTICLE_ABSTRACT); + if (StringUtils.isBlank(content)) { + if (StringUtils.isNotBlank(article.optString(Article.ARTICLE_VIEW_PWD))) { + return ""; + } + + content = article.optString(Article.ARTICLE_CONTENT); + } + + return getAbstractText(content); + } +} diff --git a/src/main/java/org/b3log/solo/model/Category.java b/src/main/java/org/b3log/solo/model/Category.java new file mode 100644 index 00000000..465619db --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Category.java @@ -0,0 +1,80 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +/** + * This class defines all category model relevant keys. + * + * @author Liang Ding + * @version 1.2.0.0, Sep 11, 2019 + * @since 2.0.0 + */ +public final class Category { + + /** + * Category. + */ + public static final String CATEGORY = "category"; + + /** + * Categories. + */ + public static final String CATEGORIES = "categories"; + + /** + * Key of category title. + */ + public static final String CATEGORY_TITLE = "categoryTitle"; + + /** + * Key of category URI. + */ + public static final String CATEGORY_URI = "categoryURI"; + + /** + * Key of category description. + */ + public static final String CATEGORY_DESCRIPTION = "categoryDescription"; + + /** + * Key of category order. + */ + public static final String CATEGORY_ORDER = "categoryOrder"; + + /** + * Key of category tag count. + */ + public static final String CATEGORY_TAG_CNT = "categoryTagCnt"; + + //// Transient //// + /** + * Key of category tags. + */ + public static final String CATEGORY_T_TAGS = "categoryTags"; + + /** + * Key of category published article count. + */ + public static final String CATEGORY_T_PUBLISHED_ARTICLE_COUNT = "categoryPublishedArticleCount"; + + /** + * Private constructor. + */ + private Category() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Comment.java b/src/main/java/org/b3log/solo/model/Comment.java new file mode 100644 index 00000000..ae3dd1fc --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Comment.java @@ -0,0 +1,126 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * This class defines all comment model relevant keys. + * + * @author Liang Ding + * @version 1.2.0.2, Apr 19, 2019 + * @since 0.3.1 + */ +public final class Comment { + + /** + * Comment. + */ + public static final String COMMENT = "comment"; + + /** + * Comments. + */ + public static final String COMMENTS = "comments"; + + /** + * Key of comment. + */ + public static final String COMMENT_CONTENT = "commentContent"; + + /** + * Key of comment name. + */ + public static final String COMMENT_NAME = "commentName"; + + /** + * Key of comment URL. + */ + public static final String COMMENT_URL = "commentURL"; + + /** + * Key of comment sharp URL. + */ + public static final String COMMENT_SHARP_URL = "commentSharpURL"; + + /** + * Key of comment created at. + */ + public static final String COMMENT_CREATED = "commentCreated"; + + /** + * Key of comment date. + */ + public static final String COMMENT_T_DATE = "commentDate"; + + /** + * Key of comment time. + */ + public static final String COMMENT_TIME = "commentTime"; + + /** + * Key of comment thumbnail URL. + */ + public static final String COMMENT_THUMBNAIL_URL = "commentThumbnailURL"; + + /** + * Key of original comment id. + */ + public static final String COMMENT_ORIGINAL_COMMENT_ID = "commentOriginalCommentId"; + + /** + * Key of original comment user name. + */ + public static final String COMMENT_ORIGINAL_COMMENT_NAME = "commentOriginalCommentName"; + + /** + * Key of comment on id. + */ + public static final String COMMENT_ON_ID = "commentOnId"; + + /** + * Gets comment sharp URL with the specified page and comment id. + * + * @param page the specified page + * @param commentId the specified comment id + * @return comment sharp URL + * @throws JSONException json exception + */ + public static String getCommentSharpURLForPage(final JSONObject page, final String commentId) throws JSONException { + return page.getString(Page.PAGE_PERMALINK) + "#" + commentId; + } + + /** + * Gets comment sharp URL with the specified article and comment id. + * + * @param article the specified article + * @param commentId the specified comment id + * @return comment sharp URL + * @throws JSONException json exception + */ + public static String getCommentSharpURLForArticle(final JSONObject article, final String commentId) throws JSONException { + return article.getString(Article.ARTICLE_PERMALINK) + "#" + commentId; + } + + /** + * Private constructor. + */ + private Comment() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Common.java b/src/main/java/org/b3log/solo/model/Common.java new file mode 100644 index 00000000..6077661a --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Common.java @@ -0,0 +1,360 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +/** + * This class defines all common model relevant keys. + * + * @author Liang Ding + * @author Dongxu Wang + * @version 1.7.0.6, Sep 17, 2019 + * @since 0.3.1 + */ +public final class Common { + + /** + * Key of skin cookie name. + */ + public static final String COOKIE_NAME_SKIN = "skin"; + + /** + * Key of mobile skin cookie name. + */ + public static final String COOKIE_NAME_MOBILE_SKIN = "mobile-skin"; + + /** + * Key of favicon URL. + */ + public static final String FAVICON_URL = "faviconURL"; + + /** + * Key of URL. + */ + public static final String URL = "url"; + + /** + * Key of referer. + */ + public static final String REFERER = "referer"; + + /** + * Key of upload msg. + */ + public static final String UPLOAD_MSG = "uploadMsg"; + + /** + * Key of upload URL. + */ + public static final String UPLOAD_URL = "uploadURL"; + + /** + * Key of upload token. + */ + public static final String UPLOAD_TOKEN = "uploadToken"; + + /** + * Key of Lute engine available. + */ + public static final String LUTE_AVAILABLE = "luteAvailable"; + + /** + * Key of keyword. + */ + public static final String KEYWORD = "keyword"; + + /** + * Key of data. + */ + public static final String DATA = "data"; + + /** + * Key of direction. + */ + public static final String DIRECTION = "direction"; + + /** + * Most used tags. + */ + public static final String MOST_USED_TAGS = "mostUsedTags"; + + /** + * Most used categories. + */ + public static final String MOST_USED_CATEGORIES = "mostUsedCategories"; + + /** + * Most comment count articles. + */ + public static final String MOST_COMMENT_ARTICLES = "mostCommentArticles"; + + /** + * Most view count articles. + */ + public static final String MOST_VIEW_COUNT_ARTICLES = "mostViewCountArticles"; + + /** + * Recent articles. + */ + public static final String RECENT_ARTICLES = "recentArticles"; + + /** + * Recent comments. + */ + public static final String RECENT_COMMENTS = "recentComments"; + + /** + * Previous article permalink. + */ + public static final String PREVIOUS_ARTICLE_PERMALINK = "previousArticlePermalink"; + + /** + * Next article permalink. + */ + public static final String NEXT_ARTICLE_PERMALINK = "nextArticlePermalink"; + + /** + * Previous article title. + */ + public static final String PREVIOUS_ARTICLE_TITLE = "previousArticleTitle"; + + /** + * Previous article abstract. + */ + public static final String PREVIOUS_ARTICLE_ABSTRACT = "previousArticleAbstract"; + + /** + * Next article title. + */ + public static final String NEXT_ARTICLE_TITLE = "nextArticleTitle"; + + /** + * Next article abstract. + */ + public static final String NEXT_ARTICLE_ABSTRACT = "nextArticleAbstract"; + + /** + * Is index. + */ + public static final String IS_INDEX = "isIndex"; + + /** + * Key of path. + */ + public static final String PATH = "path"; + + /** + * Version. + */ + public static final String VERSION = "version"; + + /** + * Static resource version. + */ + public static final String STATIC_RESOURCE_VERSION = "staticResourceVersion"; + + /** + * Year. + */ + public static final String YEAR = "year"; + + /** + * Key of flag a comment is an reply or not. + */ + public static final String IS_REPLY = "isReply"; + + /** + * Key of page navigations. + */ + public static final String PAGE_NAVIGATIONS = "pageNavigations"; + + /** + * Key of relevant articles. + */ + public static final String RELEVANT_ARTICLES = "relevantArticles"; + + /** + * Key of random articles. + */ + public static final String RANDOM_ARTICLES = "randomArticles"; + + /** + * Key of has updated. + */ + public static final String HAS_UPDATED = "hasUpdated"; + + /** + * Author name. + */ + public static final String AUTHOR_NAME = "authorName"; + + /** + * Author thumbnail URL. + */ + public static final String AUTHOR_THUMBNAIL_URL = "authorThumbnailURL"; + + /** + * Author id. + */ + public static final String AUTHOR_ID = "authorId"; + + /** + * Author role. + */ + public static final String AUTHOR_ROLE = "authorRole"; + + /** + * Key of current user. + */ + public static final String CURRENT_USER = "currentUser"; + + /** + * Key of admin user. + */ + public static final String ADMIN_USER = "adminUser"; + + /** + * Key of is logged in. + */ + public static final String IS_LOGGED_IN = "isLoggedIn"; + + /** + * Key of favicon API. + */ + public static final String FAVICON_API = "faviconAPI"; + + /** + * Key of is mobile request. + */ + public static final String IS_MOBILE_REQUEST = "isMobileRequest"; + + /** + * Key of login URL. + */ + public static final String LOGIN_URL = "loginURL"; + + /** + * Key of logout URL. + */ + public static final String LOGOUT_URL = "logoutURL"; + + /** + * Key of is administrator. + */ + public static final String IS_ADMIN = "isAdmin"; + + /** + * Key of is visitor. + */ + public static final String IS_VISITOR = "isVisitor"; + + /** + * Key of URI. + */ + public static final String URI = "URI"; + + /** + * Key of post to community. + */ + public static final String POST_TO_COMMUNITY = "postToCommunity"; + + /** + * Key of mini postfix. + */ + public static final String MINI_POSTFIX = "miniPostfix"; + + /** + * Value of mini postfix. + */ + public static final String MINI_POSTFIX_VALUE = ".min"; + + /** + * Key of month name. + */ + public static final String MONTH_NAME = "monthName"; + + /** + * Key of comment title (article/page). + */ + public static final String COMMENT_TITLE = "commentTitle"; + + /** + * /admin-index.do#main. + */ + public static final String ADMIN_INDEX_URI = "/admin-index.do#main"; + + /** + * Key of type. + */ + public static final String TYPE = "type"; + + /** + * Article comment type. + */ + public static final String ARTICLE_COMMENT_TYPE = "articleComment"; + + /** + * Page comment type. + */ + public static final String PAGE_COMMENT_TYPE = "pageComment"; + + /** + * Key of top bar replacement flag. + */ + public static final String TOP_BAR = "topBarReplacement"; + + /** + * Key of unused tags. + */ + public static final String UNUSED_TAGS = "unusedTags"; + + /** + * Key of online visitor count. + */ + public static final String ONLINE_VISITOR_CNT = "onlineVisitorCnt"; + + /** + * Key of article sign. + */ + public static final String ARTICLE_SIGN = "articleSign"; + + /** + * Key of permalink. + */ + public static final String PERMALINK = "permalink"; + + /** + * Key of commentable. + */ + public static final String COMMENTABLE = "commentable"; + + /** + * Key of articles view password. + */ + public static final String ARTICLES_VIEW_PWD = "articlesViewPwd"; + + /** + * Key of Gravatar. + */ + public static final String GRAVATAR = "gravatar"; + + /** + * Private constructor. + */ + private Common() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Link.java b/src/main/java/org/b3log/solo/model/Link.java new file mode 100644 index 00000000..691df9bb --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Link.java @@ -0,0 +1,64 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +/** + * This class defines all link model relevant keys. + * + * @author Liang Ding + * @version 1.0.0.2, Oct 31, 2011 + * @since 0.3.1 + */ +public final class Link { + + /** + * Link. + */ + public static final String LINK = "link"; + + /** + * Links. + */ + public static final String LINKS = "links"; + + /** + * Key of title. + */ + public static final String LINK_TITLE = "linkTitle"; + + /** + * Key of address. + */ + public static final String LINK_ADDRESS = "linkAddress"; + + /** + * Key of description. + */ + public static final String LINK_DESCRIPTION = "linkDescription"; + + /** + * Key of order. + */ + public static final String LINK_ORDER = "linkOrder"; + + /** + * Private constructor. + */ + private Link() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Option.java b/src/main/java/org/b3log/solo/model/Option.java new file mode 100644 index 00000000..9134ce30 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Option.java @@ -0,0 +1,494 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +import org.b3log.latke.Keys; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * This class defines option model relevant keys. + * + * @author Liang Ding + * @author hzchendou + * @version 1.6.0.2, Aug 18, 2019 + * @since 0.6.0 + */ +public final class Option { + + /** + * Option. + */ + public static final String OPTION = "option"; + + /** + * Options. + */ + public static final String OPTIONS = "options"; + + /** + * Key of option value. + */ + public static final String OPTION_VALUE = "optionValue"; + + /** + * Key of option category. + */ + public static final String OPTION_CATEGORY = "optionCategory"; + + // oId constants + /** + * Key of hljs theme. 在设置中可选择语法高亮主题 https://github.com/b3log/solo/issues/12722 + */ + public static final String ID_C_HLJS_THEME = "hljsTheme"; + + /** + * Key of enable sync (push) GitHub. 导出文章到仓库 https://hacpai.com/article/1557238327458 + */ + public static final String ID_C_SYNC_GITHUB = "syncGitHub"; + + /** + * Key of enable sync (pull) GitHub. 拉取并展示仓库 https://hacpai.com/article/1557238327458 + * https://github.com/b3log/solo/issues/12825 + */ + public static final String ID_C_PULL_GITHUB = "pullGitHub"; + + /** + * Key of favicon URL. + */ + public static final String ID_C_FAVICON_URL = "faviconURL"; + + /** + * Key of custom vars. + */ + public static final String ID_C_CUSTOM_VARS = "customVars"; + + /** + * Key of blog title. + */ + public static final String ID_C_BLOG_TITLE = "blogTitle"; + + /** + * Key of blog subtitle. + */ + public static final String ID_C_BLOG_SUBTITLE = "blogSubtitle"; + + /** + * Key of relevant articles display count. + */ + public static final String ID_C_RELEVANT_ARTICLES_DISPLAY_CNT = "relevantArticlesDisplayCount"; + + /** + * Key of random articles display count. + */ + public static final String ID_C_RANDOM_ARTICLES_DISPLAY_CNT = "randomArticlesDisplayCount"; + + /** + * Key of external relevant articles display count. + */ + public static final String ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT = "externalRelevantArticlesDisplayCount"; + + /** + * Key of recent article display count. + */ + public static final String ID_C_RECENT_ARTICLE_DISPLAY_CNT = "recentArticleDisplayCount"; + + /** + * Key of recent comment display count. + */ + public static final String ID_C_RECENT_COMMENT_DISPLAY_CNT = "recentCommentDisplayCount"; + + /** + * Key of most used tag display count. + */ + public static final String ID_C_MOST_USED_TAG_DISPLAY_CNT = "mostUsedTagDisplayCount"; + + /** + * Key of most comment article display count. + */ + public static final String ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT = "mostCommentArticleDisplayCount"; + + /** + * Key of most view article display count. + */ + public static final String ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT = "mostViewArticleDisplayCount"; + + /** + * Key of article list display count. + */ + public static final String ID_C_ARTICLE_LIST_DISPLAY_COUNT = "articleListDisplayCount"; + + /** + * Key of article list pagination window size. + */ + public static final String ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE = "articleListPaginationWindowSize"; + + /** + * Key of locale string. + */ + public static final String ID_C_LOCALE_STRING = "localeString"; + + /** + * Key of time zone id. + */ + public static final String ID_C_TIME_ZONE_ID = "timeZoneId"; + + /** + * Key of notice board. + */ + public static final String ID_C_NOTICE_BOARD = "noticeBoard"; + + /** + * Key of HTML head. + */ + public static final String ID_C_HTML_HEAD = "htmlHead"; + + /** + * Key of meta keywords. + */ + public static final String ID_C_META_KEYWORDS = "metaKeywords"; + + /** + * Key of meta description. + */ + public static final String ID_C_META_DESCRIPTION = "metaDescription"; + + /** + * Key of article update hint flag. + */ + public static final String ID_C_ENABLE_ARTICLE_UPDATE_HINT = "enableArticleUpdateHint"; + + /** + * Key of signs. + */ + public static final String ID_C_SIGNS = "signs"; + + /** + * Key of allow visit draft via permalink. + */ + public static final String ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK = "allowVisitDraftViaPermalink"; + + /** + * Key of version. + */ + public static final String ID_C_VERSION = "version"; + + /** + * Key of article list display style. + *

+ * Optional values: + *

    + *
  • "titleOnly"
  • + *
  • "titleAndContent"
  • + *
  • "titleAndAbstract"
  • + *
+ *

+ */ + public static final String ID_C_ARTICLE_LIST_STYLE = "articleListStyle"; + + /** + * Key of article/page comment-able. + */ + public static final String ID_C_COMMENTABLE = "commentable"; + + /** + * Key of feed (Atom/RSS) output mode. + *

+ * Optional values: + *

    + *
  • "abstract"
  • + *
  • "fullContent"
  • + *
+ *

+ */ + public static final String ID_C_FEED_OUTPUT_MODE = "feedOutputMode"; + + /** + * Key of feed (Atom/RSS) output entry count. + */ + public static final String ID_C_FEED_OUTPUT_CNT = "feedOutputCnt"; + + /** + * Key of skin dir name. + */ + public static final String ID_C_SKIN_DIR_NAME = "skinDirName"; + + /** + * Key of mobile skin dir name. + */ + public static final String ID_C_MOBILE_SKIN_DIR_NAME = "mobileSkinDirName"; + + /** + * Key of footer content. + */ + public static final String ID_C_FOOTER_CONTENT = "footerContent"; + + /** + * Key of statistic blog view count. + */ + public static final String ID_C_STATISTIC_BLOG_VIEW_COUNT = "statisticBlogViewCount"; + + /** + * Key of GitHub repos. + */ + public static final String ID_C_GITHUB_REPOS = "githubRepos"; + + /** + * Key of USite. + */ + public static final String ID_C_USITE = "usite"; + + // Category constants + /** + * Category - Preference. + */ + public static final String CATEGORY_C_PREFERENCE = "preference"; + + /** + * Category - Statistic. + */ + public static final String CATEGORY_C_STATISTIC = "statistic"; + + /** + * Category - GitHub. + */ + public static final String CATEGORY_C_GITHUB = "github"; + + /** + * Category - HacPai. + */ + public static final String CATEGORY_C_HACPAI = "hacpai"; + + /** + * Category - Skin. + */ + public static final String CATEGORY_C_SKIN = "skin"; + + //// Transient //// + /** + * Key of statistic blog published article count. + */ + public static final String ID_T_STATISTIC_PUBLISHED_ARTICLE_COUNT = "statisticPublishedBlogArticleCount"; + + /** + * Key of statistic blog comment(published article) count. + */ + public static final String ID_T_STATISTIC_PUBLISHED_BLOG_COMMENT_COUNT = "statisticPublishedBlogCommentCount"; + + /** + * Private constructor. + */ + private Option() { + } + + /** + * Default preference. + * + * @author Liang Ding + * @version 2.3.0.3, Sep 18, 2019 + * @since 0.3.1 + */ + public static final class DefaultPreference { + + /** + * Default hljs theme. + */ + public static final String DEFAULT_HLJS_THEME = "github"; + + /** + * Default enable sync push GitHub. + */ + public static final String DEFAULT_SYNC_GITHUB = "true"; + + /** + * Default enable sync pull GitHub. + */ + public static final String DEFAULT_PULL_GITHUB = "true"; + + /** + * Default favicon URL. + */ + public static final String DEFAULT_FAVICON_URL = "https://static.b3log.org/images/brand/solo-32.png"; + + /** + * Default custom vars. + */ + public static final String DEFAULT_CUSTOM_VARS = "key0=val0|key1=val1|key2=val2"; + + /** + * Default recent article display count. + */ + public static final int DEFAULT_RECENT_ARTICLE_DISPLAY_COUNT = 10; + + /** + * Default recent comment display count. + */ + public static final int DEFAULT_RECENT_COMMENT_DISPLAY_COUNT = 10; + + /** + * Default most used tag display count. + */ + public static final int DEFAULT_MOST_USED_TAG_DISPLAY_COUNT = 20; + + /** + * Default article list display count. + */ + public static final int DEFAULT_ARTICLE_LIST_DISPLAY_COUNT = 20; + + /** + * Default article list pagination window size. + */ + public static final int DEFAULT_ARTICLE_LIST_PAGINATION_WINDOW_SIZE = 15; + + /** + * Default most comment article display count. + */ + public static final int DEFAULT_MOST_COMMENT_ARTICLE_DISPLAY_COUNT = 5; + + /** + * Default blog subtitle. + */ + public static final String DEFAULT_BLOG_SUBTITLE = "记录精彩的程序人生"; + + /** + * Default skin directory name. + */ + public static final String DEFAULT_SKIN_DIR_NAME = "solo-nexmoe"; + + /** + * Default mobile skin directory name. + */ + public static final String DEFAULT_MOBILE_SKIN_DIR_NAME = "solo-nexmoe"; + + /** + * Default language. + */ + public static final String DEFAULT_LANGUAGE = "zh_CN"; + + /** + * Default time zone. + * + * @see java.util.TimeZone#getAvailableIDs() + */ + public static final String DEFAULT_TIME_ZONE = "Asia/Shanghai"; + + /** + * Default enable article update hint. + */ + public static final String DEFAULT_ENABLE_ARTICLE_UPDATE_HINT = "true"; + + /** + * Default notice board. + */ + public static final String DEFAULT_NOTICE_BOARD = "Open Source, Open Mind,
Open Sight, Open Future!\n\n"; + + /** + * Default meta keywords.. + */ + public static final String DEFAULT_META_KEYWORDS = "Solo,Java,博客,开源"; + + /** + * Default meta description.. + */ + public static final String DEFAULT_META_DESCRIPTION = "A small and beautiful blogging system. 一款小而美的博客系统。"; + + /** + * Default HTML head to append. + */ + public static final String DEFAULT_HTML_HEAD = ""; + + /** + * Default footer content. + */ + public static final String DEFAULT_FOOTER_CONTENT = ""; + + /** + * Default relevant articles display count. + */ + public static final int DEFAULT_RELEVANT_ARTICLES_DISPLAY_COUNT = 5; + + /** + * Default random articles display count. + */ + public static final int DEFAULT_RANDOM_ARTICLES_DISPLAY_COUNT = 5; + + /** + * Default external relevant articles display count. + */ + public static final int DEFAULT_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_COUNT = 0; + + /** + * Most view articles display count. + */ + public static final int DEFAULT_MOST_VIEW_ARTICLES_DISPLAY_COUNT = 5; + + /** + * Default signs. + */ + public static final String DEFAULT_SIGNS; + + /** + * Default allow visit draft via permalink. + */ + public static final String DEFAULT_ALLOW_VISIT_DRAFT_VIA_PERMALINK = "false"; + + /** + * Default allow comment article/page. + */ + public static final String DEFAULT_COMMENTABLE = "true"; + + /** + * Default article list display style. + */ + public static final String DEFAULT_ARTICLE_LIST_STYLE = "titleAndAbstract"; + + /** + * Default feed output mode. + */ + public static final String DEFAULT_FEED_OUTPUT_MODE = "abstract"; + + /** + * Default feed output entry count. + */ + public static final int DEFAULT_FEED_OUTPUT_CNT = 10; + + static { + final JSONArray signs = new JSONArray(); + for (int i = 0; i < 4; i++) { + final JSONObject sign = new JSONObject(); + sign.put(Keys.OBJECT_ID, i); + signs.put(sign); + String html = "
\n\n"; + html += "标题:{title}
\n"; + html += "作者:{author}
\n"; + html += "地址:{url}
\n\n"; + html += "\n
"; + sign.put(Sign.SIGN_HTML, html); + } + + // Sign(id=0) is the 'empty' sign, used for article user needn't a sign + DEFAULT_SIGNS = signs.toString(); + } + + /** + * Private constructor. + */ + private DefaultPreference() { + } + } +} diff --git a/src/main/java/org/b3log/solo/model/Page.java b/src/main/java/org/b3log/solo/model/Page.java new file mode 100644 index 00000000..6974e001 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Page.java @@ -0,0 +1,85 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +/** + * This class defines all page model relevant keys. + * + * @author Liang Ding + * @version 1.2.0.0, Apr 19, 2019 + * @since 0.3.1 + */ +public final class Page { + + /** + * Page. + */ + public static final String PAGE = "page"; + + /** + * Pages. + */ + public static final String PAGES = "pages"; + + /** + * Key of title. + */ + public static final String PAGE_TITLE = "pageTitle"; + + /** + * Key of order. + */ + public static final String PAGE_ORDER = "pageOrder"; + + /** + * Key of permalink. + */ + public static final String PAGE_PERMALINK = "pagePermalink"; + + /** + * Key of open target. + *

+ * Available values: + *

    + *
  • _blank
  • + * Opens the linked document in a new window or tab. + *
  • _self
  • + * Opens the linked document in the same frame as it was clicked (this is default). + *
  • _parent
  • + * Opens the linked document in the parent frame. + *
  • _top
  • + * Opens the linked document in the full body of the window. + *
  • frame name
  • + * Opens the linked document in a named frame. + *
+ * See here for more details. + *

+ */ + public static final String PAGE_OPEN_TARGET = "pageOpenTarget"; + + /** + * Key of icon URL. + */ + public static final String PAGE_ICON = "pageIcon"; + + /** + * Private constructor. + */ + private Page() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Sign.java b/src/main/java/org/b3log/solo/model/Sign.java new file mode 100644 index 00000000..63942501 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Sign.java @@ -0,0 +1,48 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +/** + * This class defines all sign model relevant keys. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 29, 2010 + */ +public final class Sign { + + /** + * Key of sign. + */ + public static final String SIGN = "sign"; + + /** + * Key of signs. + */ + public static final String SIGNS = "signs"; + + /** + * Key of sign HTML. + */ + public static final String SIGN_HTML = "signHTML"; + + /** + * Private constructor. + */ + private Sign() { + } +} diff --git a/src/main/java/org/b3log/solo/model/Tag.java b/src/main/java/org/b3log/solo/model/Tag.java new file mode 100644 index 00000000..35a9844c --- /dev/null +++ b/src/main/java/org/b3log/solo/model/Tag.java @@ -0,0 +1,151 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.util.Strings; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * This class defines all tag model relevant keys. + * + * @author Liang Ding + * @version 1.1.0.4, Jul 22, 2019 + */ +public final class Tag { + + /** + * Tag. + */ + public static final String TAG = "tag"; + + /** + * Tags. + */ + public static final String TAGS = "tags"; + + /** + * Key of title. + */ + public static final String TAG_TITLE = "tagTitle"; + + //// Transient //// + /** + * Key of tag reference (published article) count. + */ + public static final String TAG_T_PUBLISHED_REFERENCE_COUNT = "tagPublishedRefCount"; + + /** + * Tag title pattern string. + */ + public static final String TAG_TITLE_PATTERN_STR = "[\\u4e00-\\u9fa5\\w&#+\\-.]+"; + + /** + * Tag title pattern. + */ + public static final Pattern TAG_TITLE_PATTERN = Pattern.compile(TAG_TITLE_PATTERN_STR); + + /** + * Max length of a tag. + */ + public static final int MAX_LENGTH = 16; + + /** + * Formats the specified tags. + *
    + *
  • Trims every tag
  • + *
  • Deduplication
  • + *
+ * + * @param tagStr the specified tags + * @param maxTagCount the specified max tag count + * @return formatted tags string + */ + public static String formatTags(final String tagStr, final int maxTagCount) { + final String tagStr1 = tagStr.replaceAll("\\s+", "").replaceAll(",", ",").replaceAll("、", ","). + replaceAll(";", ",").replaceAll(";", ","); + String[] tagTitles = tagStr1.split(","); + + tagTitles = Strings.trimAll(tagTitles); + + // deduplication + final Set titles = new LinkedHashSet<>(); + for (final String tagTitle : tagTitles) { + if (!exists(titles, tagTitle)) { + titles.add(tagTitle); + } + } + + tagTitles = titles.toArray(new String[0]); + + int count = 0; + final StringBuilder tagsBuilder = new StringBuilder(); + for (final String tagTitle : tagTitles) { + String title = tagTitle.trim(); + if (StringUtils.isBlank(title)) { + continue; + } + + if (StringUtils.length(title) > MAX_LENGTH) { + continue; + } + + if (!TAG_TITLE_PATTERN.matcher(title).matches()) { + continue; + } + + tagsBuilder.append(title).append(","); + count++; + + if (maxTagCount <= count) { + break; + } + } + if (tagsBuilder.length() > 0) { + tagsBuilder.deleteCharAt(tagsBuilder.length() - 1); + } + + return tagsBuilder.toString(); + } + + /** + * Checks the specified title exists in the specified title set. + * + * @param titles the specified title set + * @param title the specified title to check + * @return {@code true} if exists, returns {@code false} otherwise + */ + private static boolean exists(final Set titles, final String title) { + for (final String setTitle : titles) { + if (setTitle.equalsIgnoreCase(title)) { + return true; + } + } + + return false; + } + + /** + * Private constructor. + */ + private Tag() { + } +} diff --git a/src/main/java/org/b3log/solo/model/UserExt.java b/src/main/java/org/b3log/solo/model/UserExt.java new file mode 100644 index 00000000..913bb348 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/UserExt.java @@ -0,0 +1,95 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model; + +import org.apache.commons.lang.StringUtils; + +/** + * This class defines ext of user model relevant keys. + * + * @author Liang Ding + * @version 1.3.0.0, Feb 8, 2019 + * @see org.b3log.latke.model.User + * @since 0.4.1 + */ +public final class UserExt { + + /** + * Key of user avatar. + */ + public static final String USER_AVATAR = "userAvatar"; + + /** + * Max user name length. + */ + public static final int MAX_USER_NAME_LENGTH = 64; + + /** + * Min user name length. + */ + public static final int MIN_USER_NAME_LENGTH = 1; + + /** + * Key of user B3 key. + */ + public static final String USER_B3_KEY = "userB3Key"; + + /** + * Key of GitHub open id. + */ + public static final String USER_GITHUB_ID = "userGitHubId"; + + /** + * Checks whether the specified name is invalid. + *

+ * A valid user name: + *

    + *
  • length [1, 64]
  • + *
  • content {a-z, A-Z, 0-9, -}
  • + *
  • Not contains "admin"/"Admin"
  • + *
+ *

+ * + * @param name the specified name + * @return {@code true} if it is invalid, returns {@code false} otherwise + */ + public static boolean invalidUserName(final String name) { + final int length = name.length(); + if (length < MIN_USER_NAME_LENGTH || length > MAX_USER_NAME_LENGTH) { + return true; + } + + char c; + for (int i = 0; i < length; i++) { + c = name.charAt(i); + if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || '-' == c) { + continue; + } + + return true; + } + + return StringUtils.containsIgnoreCase(name, "admin"); + } + + /** + * Private constructor. + */ + private UserExt() { + } +} diff --git a/src/main/java/org/b3log/solo/model/atom/Category.java b/src/main/java/org/b3log/solo/model/atom/Category.java new file mode 100644 index 00000000..bb3dc8b5 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/atom/Category.java @@ -0,0 +1,69 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.atom; + + +import org.apache.commons.lang.StringEscapeUtils; + +/** + * Category. + * + * @author Liang Ding + * @version 1.1.1.0, Jul 5, 2018 + * @since 0.3.1 + */ +public final class Category { + + /** + * Term variable. + */ + private static final String TERM_VARIABLE = "${term}"; + + /** + * Category element. + */ + private static final String CATEGORY_ELEMENT = ""; + + /** + * Term. + */ + private String term; + + /** + * Gets the term. + * + * @return term + */ + public String getTerm() { + return term; + } + + /** + * Sets the term with the specified term. + * + * @param term the specified term + */ + public void setTerm(final String term) { + this.term = term; + } + + @Override + public String toString() { + return CATEGORY_ELEMENT.replace(TERM_VARIABLE, StringEscapeUtils.escapeXml(term)); + } +} diff --git a/src/main/java/org/b3log/solo/model/atom/Entry.java b/src/main/java/org/b3log/solo/model/atom/Entry.java new file mode 100644 index 00000000..9064ba13 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/atom/Entry.java @@ -0,0 +1,348 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.atom; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.time.DateFormatUtils; + +import java.util.*; + +/** + * Entry. + * + * @author Liang Ding + * @version 1.1.2.0, Jul 5, 2018 + * @since 0.3.1 + */ +public final class Entry { + + /** + * Link variable. + */ + private static final String LINK_VARIABLE = "${link}"; + + /** + * Start title element. + */ + private static final String START_TITLE_ELEMENT = ""; + + /** + * End title element. + */ + private static final String END_TITLE_ELEMENT = ""; + + /** + * Start author element. + */ + private static final String START_AUTHOR_ELEMENT = ""; + + /** + * End author element. + */ + private static final String END_AUTHOR_ELEMENT = ""; + + /** + * Start name element. + */ + private static final String START_NAME_ELEMENT = ""; + + /** + * End name element. + */ + private static final String END_NAME_ELEMENT = ""; + + /** + * Start URI element. + */ + private static final String START_URI_ELEMENT = ""; + + /** + * End URI element. + */ + private static final String END_URI_ELEMENT = ""; + + /** + * Start entry element. + */ + private static final String START_ENTRY_ELEMENT = ""; + + /** + * End entry element. + */ + private static final String END_ENTRY_ELEMENT = ""; + + /** + * Start id element. + */ + private static final String START_ID_ELEMENT = ""; + + /** + * End id element. + */ + private static final String END_ID_ELEMENT = ""; + + /** + * Start summary element. + */ + private static final String START_SUMMARY_ELEMENT = ""; + + /** + * End summary element. + */ + private static final String END_SUMMARY_ELEMENT = ""; + + /** + * Link element. + */ + private static final String LINK_ELEMENT = ""; + + /** + * Start updated element. + */ + private static final String START_UPDATED_ELEMENT = ""; + + /** + * End updated element. + */ + private static final String END_UPDATED_ELEMENT = ""; + + /** + * Id. + */ + private String id; + + /** + * Update date. + */ + private Date updated; + + /** + * Title. + */ + private String title; + + /** + * Summary. + */ + private String summary; + + /** + * Link. + */ + private String link; + + /** + * Author. + */ + private String author; + + /** + * URI. + */ + private String uri; + + /** + * Categories. + */ + private Set categories = new HashSet(); + + /** + * Gets the URI. + * + * @return URI + */ + public String getURI() { + return uri; + } + + /** + * Sets the URI with the specified URI. + * + * @param uri the specified URI + */ + public void setURI(final String uri) { + this.uri = uri; + } + + /** + * Gets the author. + * + * @return author + */ + public String getAuthor() { + return author; + } + + /** + * Sets the author with the specified author. + * + * @param author the specified author + */ + public void setAuthor(final String author) { + this.author = author; + } + + /** + * Gets the id. + * + * @return id + */ + public String getId() { + return id; + } + + /** + * Sets the id with the specified id. + * + * @param id the specified id + */ + public void setId(final String id) { + this.id = id; + } + + /** + * Gets the link. + * + * @return link + */ + public String getLink() { + return link; + } + + /** + * Sets the link with the specified link. + * + * @param link the specified link + */ + public void setLink(final String link) { + this.link = link; + } + + /** + * Gets the title. + * + * @return title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title with the specified title. + * + * @param title the specified title + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * Gets update date. + * + * @return update date + */ + public Date getUpdated() { + return updated; + } + + /** + * Sets the update date with the specified update date. + * + * @param updated the specified update date + */ + public void setUpdated(final Date updated) { + this.updated = updated; + } + + /** + * Gets the summary. + * + * @return summary + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary with the specified summary. + * + * @param summary the specified summary + */ + public void setSummary(final String summary) { + this.summary = summary; + } + + /** + * Gets the categories. + * + * @return categories + */ + public Set getCatetories() { + return Collections.unmodifiableSet(categories); + } + + /** + * Adds the specified category. + * + * @param category the specified category + */ + public void addCatetory(final Category category) { + categories.add(category); + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(START_ENTRY_ELEMENT).append(START_TITLE_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(title)); + stringBuilder.append(END_TITLE_ELEMENT); + + stringBuilder.append(START_AUTHOR_ELEMENT); + stringBuilder.append(START_NAME_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(author)); + stringBuilder.append(END_NAME_ELEMENT); + stringBuilder.append(START_URI_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(uri)); + stringBuilder.append(END_URI_ELEMENT); + stringBuilder.append(END_AUTHOR_ELEMENT); + + for (final Category category : categories) { + stringBuilder.append(category.toString()); + } + + stringBuilder.append(LINK_ELEMENT.replace(LINK_VARIABLE, StringEscapeUtils.escapeXml(link))); + + stringBuilder.append(START_ID_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(id)); + stringBuilder.append(END_ID_ELEMENT); + + stringBuilder.append(START_UPDATED_ELEMENT); + stringBuilder.append(DateFormatUtils.format(// using ISO-8601 instead of RFC-3339 + updated, DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.getPattern(), TimeZone.getTimeZone(Feed.TIME_ZONE_ID))); + stringBuilder.append(END_UPDATED_ELEMENT); + + stringBuilder.append(START_SUMMARY_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(summary)); + stringBuilder.append(END_SUMMARY_ELEMENT); + + stringBuilder.append(END_ENTRY_ELEMENT); + + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/b3log/solo/model/atom/Feed.java b/src/main/java/org/b3log/solo/model/atom/Feed.java new file mode 100644 index 00000000..7af15bce --- /dev/null +++ b/src/main/java/org/b3log/solo/model/atom/Feed.java @@ -0,0 +1,332 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.atom; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.util.XMLs; +import org.b3log.solo.processor.FeedProcessor; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +/** + * Atom Feed. + *

+ * See RFC 4278 for more details. + *

+ * + * @author Liang Ding + * @version 1.1.1.1, Aug 2, 2018 + * @see Entry + * @see Category + * @since 0.3.1 + */ +public final class Feed { + + /** + * Time zone id. + */ + public static final String TIME_ZONE_ID = "Asia/Shanghai"; + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(FeedProcessor.class); + + /** + * Link variable. + */ + private static final String LINK_VARIABLE = "${link}"; + + /** + * Start document. + */ + private static final String START_DOCUMENT = ""; + + /** + * Start feed element. + */ + private static final String START_FEED_ELEMENT = ""; + + /** + * End feed element. + */ + private static final String END_FEED_ELEMENT = ""; + + /** + * Start id element. + */ + private static final String START_ID_ELEMENT = ""; + + /** + * End if element. + */ + private static final String END_ID_ELEMENT = ""; + + /** + * Start title element. + */ + private static final String START_TITLE_ELEMENT = ""; + + /** + * End title element. + */ + private static final String END_TITLE_ELEMENT = ""; + + /** + * Start subtitle element. + */ + private static final String START_SUBTITLE_ELEMENT = " "; + + /** + * End subtitle element. + */ + private static final String END_SUBTITLE_ELEMENT = ""; + + /** + * Start updated element. + */ + private static final String START_UPDATED_ELEMENT = ""; + + /** + * End updated element. + */ + private static final String END_UPDATED_ELEMENT = ""; + + /** + * Start author element. + */ + private static final String START_AUTHOR_ELEMENT = ""; + + /** + * End author element. + */ + private static final String END_AUTHOR_ELEMENT = ""; + + /** + * Start name element. + */ + private static final String START_NAME_ELEMENT = ""; + + /** + * End name element. + */ + private static final String END_NAME_ELEMENT = ""; + + /** + * Link element. + */ + private static final String LINK_ELEMENT = ""; + + /** + * Id. + */ + private String id; + + /** + * Title. + */ + private String title; + + /** + * Subtitle. + */ + private String subtitle; + + /** + * Update date. + */ + private Date updated; + + /** + * Author. + */ + private String author; + + /** + * Link. + */ + private String link; + + /** + * Entries. + */ + private List entries = new ArrayList<>(); + + /** + * Gets the id. + * + * @return id + */ + public String getId() { + return id; + } + + /** + * Sets the id with the specified id. + * + * @param id the specified id + */ + public void setId(final String id) { + this.id = id; + } + + /** + * Gets the link. + * + * @return link + */ + public String getLink() { + return link; + } + + /** + * Sets the link with the specified link. + * + * @param link the specified link + */ + public void setLink(final String link) { + this.link = link; + } + + /** + * Gets the title. + * + * @return title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title with the specified title. + * + * @param title the specified title + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * Gets the subtitle. + * + * @return subtitle + */ + public String getSubtitle() { + return subtitle; + } + + /** + * Sets the subtitle with the specified subtitle. + * + * @param subtitle the specified subtitle + */ + public void setSubtitle(final String subtitle) { + this.subtitle = subtitle; + } + + /** + * Gets the author. + * + * @return author + */ + public String getAuthor() { + return author; + } + + /** + * Sets the author with the specified author. + * + * @param author the specified author + */ + public void setAuthor(final String author) { + this.author = author; + } + + /** + * Gets update date. + * + * @return update date + */ + public Date getUpdated() { + return updated; + } + + /** + * Sets the update date with the specified update date. + * + * @param updated the specified update date + */ + public void setUpdated(final Date updated) { + this.updated = updated; + } + + /** + * Adds the specified entry. + * + * @param entry the specified entry + */ + public void addEntry(final Entry entry) { + entries.add(entry); + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(START_DOCUMENT); + stringBuilder.append(START_FEED_ELEMENT); + + stringBuilder.append(START_ID_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(id)); + stringBuilder.append(END_ID_ELEMENT); + + stringBuilder.append(START_TITLE_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(title)); + stringBuilder.append(END_TITLE_ELEMENT); + + stringBuilder.append(START_SUBTITLE_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(subtitle)); + stringBuilder.append(END_SUBTITLE_ELEMENT); + + stringBuilder.append(START_UPDATED_ELEMENT); + stringBuilder.append(DateFormatUtils.format(// using ISO-8601 instead of RFC-3339 + updated, DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.getPattern(), TimeZone.getTimeZone(TIME_ZONE_ID))); + stringBuilder.append(END_UPDATED_ELEMENT); + + stringBuilder.append(START_AUTHOR_ELEMENT); + stringBuilder.append(START_NAME_ELEMENT); + stringBuilder.append(author); + stringBuilder.append(END_NAME_ELEMENT); + stringBuilder.append(END_AUTHOR_ELEMENT); + + stringBuilder.append(LINK_ELEMENT.replace(LINK_VARIABLE, StringEscapeUtils.escapeXml(link))); + + for (final Entry entry : entries) { + stringBuilder.append(entry.toString()); + } + + stringBuilder.append(END_FEED_ELEMENT); + + return XMLs.format(stringBuilder.toString()); + } +} diff --git a/src/main/java/org/b3log/solo/model/package-info.java b/src/main/java/org/b3log/solo/model/package-info.java new file mode 100644 index 00000000..fd6d9a91 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/package-info.java @@ -0,0 +1,4 @@ +/** + * Keys. + */ +package org.b3log.solo.model; diff --git a/src/main/java/org/b3log/solo/model/rss/Category.java b/src/main/java/org/b3log/solo/model/rss/Category.java new file mode 100644 index 00000000..4fc52a6d --- /dev/null +++ b/src/main/java/org/b3log/solo/model/rss/Category.java @@ -0,0 +1,63 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.rss; + +import org.apache.commons.lang.StringEscapeUtils; + +/** + * Category. + * + * @author Liang Ding + * @version 1.1.1.0, Jul 5, 2018 + * @since 0.3.1 + */ +public final class Category { + + /** + * Category element. + */ + private static final String CATEGORY_ELEMENT = "${term}"; + + /** + * Term. + */ + private String term; + + /** + * Gets the term. + * + * @return term + */ + public String getTerm() { + return term; + } + + /** + * Sets the term with the specified term. + * + * @param term the specified term + */ + public void setTerm(final String term) { + this.term = term; + } + + @Override + public String toString() { + return CATEGORY_ELEMENT.replace("${term}", StringEscapeUtils.escapeXml(term)); + } +} diff --git a/src/main/java/org/b3log/solo/model/rss/Channel.java b/src/main/java/org/b3log/solo/model/rss/Channel.java new file mode 100644 index 00000000..134f5b94 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/rss/Channel.java @@ -0,0 +1,339 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.rss; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.util.XMLs; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * RSS 2.0 channel. + *

+ * See RSS 2.0 at Harvard Law for more details. + *

+ * + * @author Liang Ding + * @version 1.1.1.2, Aug 2, 2018 + * @see Item + * @see Category + * @since 0.3.1 + */ +public final class Channel { + + /** + * Start. + */ + private static final String START = ""; + + /** + * End. + */ + private static final String END = ""; + + /** + * Start title element. + */ + private static final String START_TITLE_ELEMENT = ""; + + /** + * End title element. + */ + private static final String END_TITLE_ELEMENT = ""; + + /** + * Start link element. + */ + private static final String START_LINK_ELEMENT = ""; + + /** + * Atom link variable. + */ + private static final String ATOM_LINK_VARIABLE = "${atomLink}"; + + /** + * End link element. + */ + private static final String END_LINK_ELEMENT = ""; + + /** + * Atom link element. + */ + private static final String ATOM_LINK_ELEMENT = ""; + + /** + * Start description element. + */ + private static final String START_DESCRIPTION_ELEMENT = ""; + + /** + * End description element. + */ + private static final String END_DESCRIPTION_ELEMENT = ""; + + /** + * Start generator element. + */ + private static final String START_GENERATOR_ELEMENT = ""; + + /** + * End generator element. + */ + private static final String END_GENERATOR_ELEMENT = ""; + + /** + * Start language element. + */ + private static final String START_LANGUAGE_ELEMENT = ""; + + /** + * End language element. + */ + private static final String END_LANGUAGE_ELEMENT = ""; + + /** + * Start last build date element. + */ + private static final String START_LAST_BUILD_DATE_ELEMENT = ""; + + /** + * End last build date element. + */ + private static final String END_LAST_BUILD_DATE_ELEMENT = ""; + + /** + * Title. + */ + private String title; + + /** + * Link. + */ + private String link; + + /** + * Atom link. + */ + private String atomLink; + + /** + * Description. + */ + private String description; + + /** + * Generator. + */ + private String generator; + + /** + * Last build date. + */ + private Date lastBuildDate; + + /** + * Language. + */ + private String language; + + /** + * Items. + */ + private List items = new ArrayList<>(); + + /** + * Gets the atom link. + * + * @return atom link + */ + public String getAtomLink() { + return atomLink; + } + + /** + * Sets the atom link with the specified atom link. + * + * @param atomLink the specified atom link + */ + public void setAtomLink(final String atomLink) { + this.atomLink = atomLink; + } + + /** + * Gets the last build date. + * + * @return last build date + */ + public Date getLastBuildDate() { + return lastBuildDate; + } + + /** + * Sets the last build date with the specified last build date. + * + * @param lastBuildDate the specified last build date + */ + public void setLastBuildDate(final Date lastBuildDate) { + this.lastBuildDate = lastBuildDate; + } + + /** + * Gets generator. + * + * @return generator + */ + public String getGenerator() { + return generator; + } + + /** + * Sets the generator with the specified generator. + * + * @param generator the specified generator + */ + public void setGenerator(final String generator) { + this.generator = generator; + } + + /** + * Gets the link. + * + * @return link + */ + public String getLink() { + return link; + } + + /** + * Sets the link with the specified link. + * + * @param link the specified link + */ + public void setLink(final String link) { + this.link = link; + } + + /** + * Gets the title. + * + * @return title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title with the specified title. + * + * @param title the specified title + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * Adds the specified item. + * + * @param item the specified item + */ + public void addItem(final Item item) { + items.add(item); + } + + /** + * Gets the description. + * + * @return description + */ + public String getDescription() { + return description; + } + + /** + * Sets the description with the specified description. + * + * @param description the specified description + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Gets the language. + * + * @return language + */ + public String getLanguage() { + return language; + } + + /** + * Sets the language with the specified language. + * + * @param language the specified language + */ + public void setLanguage(final String language) { + this.language = language; + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(START); + + stringBuilder.append(START_TITLE_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(title)); + stringBuilder.append(END_TITLE_ELEMENT); + + stringBuilder.append(START_LINK_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(link)); + stringBuilder.append(END_LINK_ELEMENT); + + stringBuilder.append(ATOM_LINK_ELEMENT.replace(ATOM_LINK_VARIABLE, atomLink)); + + stringBuilder.append(START_DESCRIPTION_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(description)); + stringBuilder.append(END_DESCRIPTION_ELEMENT); + + stringBuilder.append(START_GENERATOR_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(generator)); + stringBuilder.append(END_GENERATOR_ELEMENT); + + stringBuilder.append(START_LAST_BUILD_DATE_ELEMENT); + stringBuilder.append(DateFormatUtils.SMTP_DATETIME_FORMAT.format(lastBuildDate)); + stringBuilder.append(END_LAST_BUILD_DATE_ELEMENT); + + stringBuilder.append(START_LANGUAGE_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(language)); + stringBuilder.append(END_LANGUAGE_ELEMENT); + + for (final Item item : items) { + stringBuilder.append(item.toString()); + } + + stringBuilder.append(END); + + return XMLs.format(stringBuilder.toString()); + } +} diff --git a/src/main/java/org/b3log/solo/model/rss/Item.java b/src/main/java/org/b3log/solo/model/rss/Item.java new file mode 100644 index 00000000..55c7dcec --- /dev/null +++ b/src/main/java/org/b3log/solo/model/rss/Item.java @@ -0,0 +1,283 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.rss; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.time.DateFormatUtils; + +import java.util.Date; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Item. + * + * @author Liang Ding + * @version 1.1.3.1, Jul 29, 2019 + * @since 0.3.1 + */ +public final class Item { + + /** + * Start title element. + */ + private static final String START_TITLE_ELEMENT = ""; + + /** + * End title element. + */ + private static final String END_TITLE_ELEMENT = ""; + + /** + * Start link element. + */ + private static final String START_LINK_ELEMENT = ""; + + /** + * End link element. + */ + private static final String END_LINK_ELEMENT = ""; + + /** + * Start description element. + */ + private static final String START_DESCRIPTION_ELEMENT = ""; + + /** + * End summary element. + */ + private static final String END_DESCRIPTION_ELEMENT = ""; + + /** + * Start author element. + */ + private static final String START_AUTHOR_ELEMENT = ""; + + /** + * End author element. + */ + private static final String END_AUTHOR_ELEMENT = ""; + + /** + * Categories. + */ + private Set categories = new HashSet<>(); + + /** + * Start guid element. + */ + private static final String START_GUID_ELEMENT = ""; + + /** + * End guid element. + */ + private static final String END_GUID_ELEMENT = ""; + + /** + * Start pubDate element. + */ + private static final String START_PUB_DATE_ELEMENT = ""; + + /** + * End pubDate element. + */ + private static final String END_PUB_DATE_ELEMENT = ""; + + /** + * Guid. + */ + private String guid; + + /** + * Publish date. + */ + private Date pubDate; + + /** + * Title. + */ + private String title; + + /** + * Description. + */ + private String description; + + /** + * Link. + */ + private String link; + + /** + * Author. + */ + private String author; + + /** + * Gets the GUID. + * + * @return GUID + */ + public String getGUID() { + return guid; + } + + /** + * Sets the GUID with the specified GUID. + * + * @param guid the specified GUID + */ + public void setGUID(final String guid) { + this.guid = guid; + } + + /** + * Gets the author. + * + * @return author + */ + public String getAuthor() { + return author; + } + + /** + * Sets the author with the specified author. + * + * @param author the specified author + */ + public void setAuthor(final String author) { + this.author = author; + } + + /** + * Gets the link. + * + * @return link + */ + public String getLink() { + return link; + } + + /** + * Sets the link with the specified link. + * + * @param link the specified link + */ + public void setLink(final String link) { + this.link = link; + } + + /** + * Gets the title. + * + * @return title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title with the specified title. + * + * @param title the specified title + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * Gets publish date. + * + * @return publish date + */ + public Date getPubDate() { + return pubDate; + } + + /** + * Sets the publish date with the specified publish date. + * + * @param pubDate the specified publish date + */ + public void setPubDate(final Date pubDate) { + this.pubDate = pubDate; + } + + /** + * Gets the description. + * + * @return description + */ + public String getDescription() { + return description; + } + + /** + * Sets the description with the specified description. + * + * @param description the specified description + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Adds the specified category. + * + * @param category the specified category + */ + public void addCatetory(final Category category) { + categories.add(category); + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("").append(START_TITLE_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(title)); + stringBuilder.append(END_TITLE_ELEMENT); + + stringBuilder.append(START_LINK_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(link)); + stringBuilder.append(END_LINK_ELEMENT); + + stringBuilder.append(START_DESCRIPTION_ELEMENT); + stringBuilder.append(""); + stringBuilder.append(END_DESCRIPTION_ELEMENT); + + stringBuilder.append(START_AUTHOR_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(author)); + stringBuilder.append(END_AUTHOR_ELEMENT); + + stringBuilder.append(START_GUID_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(guid)); + stringBuilder.append(END_GUID_ELEMENT); + + for (final Category category : categories) { + stringBuilder.append(category.toString()); + } + + stringBuilder.append(START_PUB_DATE_ELEMENT); + stringBuilder.append(DateFormatUtils.format(pubDate, "EEE, dd MMM yyyy HH:mm:ss Z", Locale.US)); + stringBuilder.append(END_PUB_DATE_ELEMENT).append(""); + + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/b3log/solo/model/sitemap/Sitemap.java b/src/main/java/org/b3log/solo/model/sitemap/Sitemap.java new file mode 100644 index 00000000..36773067 --- /dev/null +++ b/src/main/java/org/b3log/solo/model/sitemap/Sitemap.java @@ -0,0 +1,84 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.sitemap; + + +import java.util.ArrayList; +import java.util.List; + + +/** + * Sitemap. + * + *

+ * See Sitemap XML format + * for more details. + *

+ * + * @author Liang Ding + * @version 1.1.0.0, Sep 22, 2011 + * @see URL + * @since 0.3.1 + */ +public final class Sitemap { + + /** + * Start document. + */ + private static final String START_DOCUMENT = ""; + + /** + * Start URL set element. + */ + private static final String START_URL_SET_ELEMENT = ""; + + /** + * End URL set element. + */ + private static final String END_URL_SET_ELEMENT = ""; + + /** + * URLs. + */ + private List urls = new ArrayList<>(); + + /** + * Adds the specified url. + * + * @param url the specified url + */ + public void addURL(final URL url) { + urls.add(url); + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(START_DOCUMENT); + stringBuilder.append(START_URL_SET_ELEMENT); + + for (final URL url : urls) { + stringBuilder.append(url.toString()); + } + + stringBuilder.append(END_URL_SET_ELEMENT); + + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/b3log/solo/model/sitemap/URL.java b/src/main/java/org/b3log/solo/model/sitemap/URL.java new file mode 100644 index 00000000..ca78d28c --- /dev/null +++ b/src/main/java/org/b3log/solo/model/sitemap/URL.java @@ -0,0 +1,128 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.model.sitemap; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; + +/** + * Sitemap URL. + * + * @author Liang Ding + * @version 1.1.1.0, May 18, 2018 + * @since 0.3.1 + */ +public final class URL { + + /** + * Start URL element. + */ + private static final String START_URL_ELEMENT = ""; + + /** + * End URL element. + */ + private static final String END_URL_ELEMENT = ""; + + /** + * Start loc element. + */ + private static final String START_LOC_ELEMENT = ""; + + /** + * End loc element. + */ + private static final String END_LOC_ELEMENT = ""; + + /** + * Start last mod element. + */ + private static final String START_LAST_MOD_ELEMENT = ""; + + /** + * End last mod element. + */ + private static final String END_LAST_MOD_ELEMENT = ""; + + /** + * Loc. + */ + private String loc; + + /** + * Last mod. + */ + private String lastMod; + + /** + * Gets the last modified. + * + * @return last modified + */ + public String getLastMod() { + return lastMod; + } + + /** + * Sets the last modified with the specified last modified. + * + * @param lastMod the specified modified + */ + public void setLastMod(final String lastMod) { + this.lastMod = lastMod; + } + + /** + * Gets the loc. + * + * @return loc + */ + public String getLoc() { + return loc; + } + + /** + * Sets the loc with the specified loc. + * + * @param loc the specified loc + */ + public void setLoc(final String loc) { + this.loc = loc; + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append(START_URL_ELEMENT); + + stringBuilder.append(START_LOC_ELEMENT); + stringBuilder.append(StringEscapeUtils.escapeXml(loc)); + stringBuilder.append(END_LOC_ELEMENT); + + if (StringUtils.isNotBlank(lastMod)) { + stringBuilder.append(START_LAST_MOD_ELEMENT); + stringBuilder.append(lastMod); + stringBuilder.append(END_LAST_MOD_ELEMENT); + } + + stringBuilder.append(END_URL_ELEMENT); + + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/b3log/solo/plugin/ToCPlugin.java b/src/main/java/org/b3log/solo/plugin/ToCPlugin.java new file mode 100644 index 00000000..dc0e5ea5 --- /dev/null +++ b/src/main/java/org/b3log/solo/plugin/ToCPlugin.java @@ -0,0 +1,105 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.plugin; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.event.AbstractEventListener; +import org.b3log.latke.event.Event; +import org.b3log.latke.plugin.NotInteractivePlugin; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.solo.event.EventTypes; +import org.b3log.solo.model.Article; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Parser; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * ToC event handler. + * + * @author Liang Ding + * @version 2.0.0.0, Jul 29, 2019 + * @since 0.6.7 + */ +public class ToCPlugin extends NotInteractivePlugin { + + /** + * Public constructor. + */ + public ToCPlugin() { + addEventListener(new ToCEventHandler()); + } + + @Override + public void prePlug(RequestContext context) { + } + + @Override + public void postPlug(Map dataModel, RequestContext context) { + } +} + +/** + * ToC event handler. + * + * @author Liang Ding + * @author Ann Peter + * @author Vanessa + * @version 2.0.0.0, Jul 29, 2019 + * @since 0.6.7 + */ +class ToCEventHandler extends AbstractEventListener { + + @Override + public String getEventType() { + return EventTypes.BEFORE_RENDER_ARTICLE; + } + + @Override + public void action(final Event event) { + final JSONObject data = event.getData(); + final JSONObject article = data.optJSONObject(Article.ARTICLE); + final String content = article.optString(Article.ARTICLE_CONTENT); + final Document doc = Jsoup.parse(content, StringUtils.EMPTY, Parser.htmlParser()); + doc.outputSettings().prettyPrint(false); + + final List toc = new ArrayList<>(); + final Elements hs = doc.select("h1, h2, h3, h4, h5"); + for (int i = 0; i < hs.size(); i++) { + final Element element = hs.get(i); + final String tagName = element.tagName().toLowerCase(); + final String text = element.text(); + final String id = "b3_solo_" + tagName + "_" + i; + element.attr("id", id); + final JSONObject li = new JSONObject(). + put("className", "toc__" + tagName). + put("id", id). + put("text", text); + toc.add(li); + } + final Element body = doc.getElementsByTag("body").get(0); + article.put(Article.ARTICLE_CONTENT, body.html()); + article.put(Article.ARTICLE_T_TOC, (Object) toc); + } +} diff --git a/src/main/java/org/b3log/solo/plugin/package-info.java b/src/main/java/org/b3log/solo/plugin/package-info.java new file mode 100644 index 00000000..95b433a9 --- /dev/null +++ b/src/main/java/org/b3log/solo/plugin/package-info.java @@ -0,0 +1,4 @@ +/** + * Plugins. + */ +package org.b3log.solo.plugin; diff --git a/src/main/java/org/b3log/solo/processor/ArticleProcessor.java b/src/main/java/org/b3log/solo/processor/ArticleProcessor.java new file mode 100644 index 00000000..3fce3622 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/ArticleProcessor.java @@ -0,0 +1,965 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.event.Event; +import org.b3log.latke.event.EventManager; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.User; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.latke.servlet.renderer.TextHtmlRenderer; +import org.b3log.latke.util.*; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.event.EventTypes; +import org.b3log.solo.model.*; +import org.b3log.solo.processor.console.ConsoleRenderer; +import org.b3log.solo.service.*; +import org.b3log.solo.util.Markdowns; +import org.b3log.solo.util.Skins; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.jsoup.Jsoup; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.*; + +/** + * Article processor. + * + * @author Liang Ding + * @author Zephyr + * @version 1.4.5.8, Sep 11, 2019 + * @since 0.3.1 + */ +@RequestProcessor +public class ArticleProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleProcessor.class); + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * Comment query service. + */ + @Inject + private CommentQueryService commentQueryService; + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Archive date query service. + */ + @Inject + private ArchiveDateQueryService archiveDateQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Article management service. + */ + @Inject + private ArticleMgmtService articleMgmtService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Event manager. + */ + @Inject + private EventManager eventManager; + + /** + * Markdowns. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "html": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @RequestProcessing(value = "/console/markdown/2html", method = HttpMethod.POST) + public void markdown2HTML(final RequestContext context) { + final JSONObject result = Solos.newSucc(); + context.renderJSON(result); + + final String markdownText = context.requestJSON().optString("markdownText"); + if (StringUtils.isBlank(markdownText)) { + result.put(Common.DATA, ""); + + return; + } + + if (!Solos.isLoggedIn(context)) { + result.put(Keys.CODE, -1); + result.put(Keys.MSG, langPropsService.get("getFailLabel")); + + return; + } + + try { + final String html = Markdowns.toHTML(markdownText); + result.put(Common.DATA, html); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + result.put(Keys.CODE, -1); + result.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Shows the article view password form. + * + * @param context the specified context + */ + @RequestProcessing(value = "/console/article-pwd", method = HttpMethod.GET) + public void showArticlePwdForm(final RequestContext context) { + final String articleId = context.param("articleId"); + if (StringUtils.isBlank(articleId)) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final JSONObject article = articleQueryService.getArticleById(articleId); + if (null == article) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, "article-pwd.ftl"); + final Map dataModel = renderer.getDataModel(); + dataModel.put("articleId", articleId); + dataModel.put("articlePermalink", article.optString(Article.ARTICLE_PERMALINK)); + dataModel.put("articleTitle", article.optString(Article.ARTICLE_TITLE)); + dataModel.put("articleAbstract", article.optString(Article.ARTICLE_ABSTRACT)); + final String msg = context.param(Keys.MSG); + + if (StringUtils.isNotBlank(msg)) { + dataModel.put(Keys.MSG, langPropsService.get("passwordNotMatchLabel")); + } + + final Map langs = langPropsService.getAll(Latkes.getLocale()); + dataModel.putAll(langs); + + final JSONObject preference = optionQueryService.getPreference(); + dataModel.put(Option.ID_C_BLOG_TITLE, preference.getString(Option.ID_C_BLOG_TITLE)); + dataModel.put(Common.VERSION, SoloServletListener.VERSION); + dataModel.put(Common.STATIC_RESOURCE_VERSION, Latkes.getStaticResourceVersion()); + dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR))); + + Keys.fillRuntime(dataModel); + dataModelService.fillMinified(dataModel); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + } + + /** + * Processes the article view password form submits. + * + * @param context the specified context + */ + @RequestProcessing(value = "/console/article-pwd", method = HttpMethod.POST) + public void onArticlePwdForm(final RequestContext context) { + try { + final HttpServletRequest request = context.getRequest(); + final String articleId = context.param("articleId"); + final String pwdTyped = context.param("pwdTyped"); + + final JSONObject article = articleQueryService.getArticleById(articleId); + + if (article.getString(Article.ARTICLE_VIEW_PWD).equals(pwdTyped)) { + final HttpSession session = request.getSession(); + if (null != session) { + Map viewPwds = (Map) session.getAttribute(Common.ARTICLES_VIEW_PWD); + if (null == viewPwds) { + viewPwds = new HashMap<>(); + } + + viewPwds.put(articleId, pwdTyped); + session.setAttribute(Common.ARTICLES_VIEW_PWD, viewPwds); + } + + context.sendRedirect(Latkes.getServePath() + article.getString(Article.ARTICLE_PERMALINK)); + + return; + } + + context.sendRedirect(Latkes.getServePath() + "/console/article-pwd?articleId=" + article.optString(Keys.OBJECT_ID) + "&msg=1"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Processes article view password form submits failed", e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Gets random articles with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/articles/random", method = HttpMethod.POST) + public void getRandomArticles(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + + final JSONObject preference = optionQueryService.getPreference(); + final int displayCnt = preference.getInt(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT); + if (0 == displayCnt) { + jsonObject.put(Common.RANDOM_ARTICLES, new ArrayList()); + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + + return; + } + + Stopwatchs.start("Get Random Articles"); + final List randomArticles = getRandomArticles(preference); + + jsonObject.put(Common.RANDOM_ARTICLES, randomArticles); + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + + Stopwatchs.end(); + } + + /** + * Gets relevant articles with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/article/id/{id}/relevant/articles", method = HttpMethod.GET) + public void getRelevantArticles(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + + final JSONObject preference = optionQueryService.getPreference(); + + final int displayCnt = preference.getInt(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT); + if (0 == displayCnt) { + jsonObject.put(Common.RANDOM_ARTICLES, new ArrayList()); + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + + return; + } + + final HttpServletRequest request = context.getRequest(); + Stopwatchs.start("Get Relevant Articles"); + final String articleId = context.pathVar("id"); + if (StringUtils.isBlank(articleId)) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final JSONObject article = articleQueryService.getArticleById(articleId); + if (null == article) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final List relevantArticles = articleQueryService.getRelevantArticles(article, preference); + jsonObject.put(Common.RELEVANT_ARTICLES, relevantArticles); + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + + Stopwatchs.end(); + } + + /** + * Gets article content with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/get-article-content", method = HttpMethod.GET) + public void getArticleContent(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final String articleId = context.param("id"); + if (StringUtils.isBlank(articleId)) { + return; + } + + final TextHtmlRenderer renderer = new TextHtmlRenderer(); + context.setRenderer(renderer); + + String content; + try { + content = articleQueryService.getArticleContent(context, articleId); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, "Can not get article content", e); + return; + } + + if (null == content) { + return; + } + + renderer.setContent(content); + } + + /** + * Gets articles paged with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/articles", method = HttpMethod.GET) + public void getArticlesByPage(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + final HttpServletRequest request = context.getRequest(); + final int currentPageNum = Paginator.getPage(request); + + Stopwatchs.start("Get Articles Paged [pageNum=" + currentPageNum + ']'); + try { + jsonObject.put(Keys.STATUS_CODE, true); + + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + + final StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.append(currentPageNum).append('/').append(pageSize).append('/').append(windowSize); + + final JSONObject requestJSONObject = Solos.buildPaginationRequest(pathBuilder.toString()); + requestJSONObject.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + requestJSONObject.put(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT, preference.optBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)); + final JSONObject result = articleQueryService.getArticles(requestJSONObject); + final List articles = org.b3log.latke.util.CollectionUtils.jsonArrayToList(result.getJSONArray(Article.ARTICLES)); + dataModelService.setArticlesExProperties(context, articles, preference); + + jsonObject.put(Keys.RESULTS, result); + } catch (final Exception e) { + jsonObject.put(Keys.STATUS_CODE, false); + LOGGER.log(Level.ERROR, "Gets article paged failed", e); + } finally { + Stopwatchs.end(); + } + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + } + + /** + * Gets tag articles paged with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/articles/tags/{tagTitle}", method = HttpMethod.GET) + public void getTagArticlesByPage(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + + final HttpServletRequest request = context.getRequest(); + final String tagTitle = context.pathVar("tagTitle"); + final int currentPageNum = Paginator.getPage(request); + Stopwatchs.start("Get Tag-Articles Paged [tagTitle=" + tagTitle + ", pageNum=" + currentPageNum + ']'); + try { + jsonObject.put(Keys.STATUS_CODE, true); + + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + + final JSONObject tagQueryResult = tagQueryService.getTagByTitle(tagTitle); + if (null == tagQueryResult) { + throw new Exception("Can not found tag [title=" + tagTitle + "]"); + } + + final JSONObject tag = tagQueryResult.getJSONObject(Tag.TAG); + final String tagId = tag.getString(Keys.OBJECT_ID); + final JSONObject tagArticleResult = articleQueryService.getArticlesByTag(tagId, currentPageNum, pageSize); + if (null == tagArticleResult) { + throw new Exception("Can not found tag [title=" + tagTitle + "]'s articles"); + } + + final List articles = (List) tagArticleResult.opt(Keys.RESULTS); + final int pageCount = tagArticleResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + dataModelService.setArticlesExProperties(context, articles, preference); + + final JSONObject result = new JSONObject(); + final JSONObject pagination = new JSONObject(); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + result.put(Pagination.PAGINATION, pagination); + result.put(Article.ARTICLES, articles); + jsonObject.put(Keys.RESULTS, result); + } catch (final Exception e) { + jsonObject.put(Keys.STATUS_CODE, false); + LOGGER.log(Level.ERROR, "Gets article paged failed", e); + } finally { + Stopwatchs.end(); + } + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + } + + /** + * Gets tag articles paged with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/articles/archives/{yyyy}/{MM}", method = HttpMethod.GET) + public void getArchivesArticlesByPage(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + + final HttpServletRequest request = context.getRequest(); + final String archiveDateString = context.pathVar("yyyy") + "/" + context.pathVar("MM"); + final int currentPageNum = Paginator.getPage(request); + + Stopwatchs.start("Get Archive-Articles Paged [archive=" + archiveDateString + ", pageNum=" + currentPageNum + ']'); + try { + jsonObject.put(Keys.STATUS_CODE, true); + + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + + final JSONObject archiveQueryResult = archiveDateQueryService.getByArchiveDateString(archiveDateString); + if (null == archiveQueryResult) { + throw new Exception("Can not found archive [archiveDate=" + archiveDateString + "]"); + } + + final JSONObject archiveDate = archiveQueryResult.getJSONObject(ArchiveDate.ARCHIVE_DATE); + final String archiveDateId = archiveDate.getString(Keys.OBJECT_ID); + + final int articleCount = archiveDateQueryService.getArchiveDatePublishedArticleCount(archiveDateId); + final int pageCount = (int) Math.ceil((double) articleCount / (double) pageSize); + + final List articles = articleQueryService.getArticlesByArchiveDate(archiveDateId, currentPageNum, pageSize); + dataModelService.setArticlesExProperties(context, articles, preference); + + final JSONObject result = new JSONObject(); + final JSONObject pagination = new JSONObject(); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + result.put(Pagination.PAGINATION, pagination); + result.put(Article.ARTICLES, articles); + jsonObject.put(Keys.RESULTS, result); + } catch (final Exception e) { + jsonObject.put(Keys.STATUS_CODE, false); + LOGGER.log(Level.ERROR, "Gets article paged failed", e); + } finally { + Stopwatchs.end(); + } + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + } + + /** + * Gets author articles paged with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/articles/authors/{author}", method = HttpMethod.GET) + public void getAuthorsArticlesByPage(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + + final HttpServletRequest request = context.getRequest(); + final String authorId = context.pathVar("author"); + final int currentPageNum = Paginator.getPage(request); + + Stopwatchs.start("Get Author-Articles Paged [authorId=" + authorId + ", pageNum=" + currentPageNum + ']'); + try { + jsonObject.put(Keys.STATUS_CODE, true); + + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + + final JSONObject authorRet = userQueryService.getUser(authorId); + if (null == authorRet) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final JSONObject articlesResult = articleQueryService.getArticlesByAuthorId(authorId, currentPageNum, pageSize); + final List articles = CollectionUtils.jsonArrayToList(articlesResult.optJSONArray(Keys.RESULTS)); + dataModelService.setArticlesExProperties(context, articles, preference); + final int pageCount = articlesResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + + final JSONObject result = new JSONObject(); + final JSONObject pagination = new JSONObject(); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + result.put(Pagination.PAGINATION, pagination); + result.put(Article.ARTICLES, articles); + jsonObject.put(Keys.RESULTS, result); + } catch (final Exception e) { + jsonObject.put(Keys.STATUS_CODE, false); + LOGGER.log(Level.ERROR, "Gets article paged failed", e); + } finally { + Stopwatchs.end(); + } + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + } + + /** + * Shows author articles with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/authors/{author}", method = HttpMethod.GET) + public void showAuthorArticles(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "author-articles.ftl"); + + try { + final String authorId = context.pathVar("author"); + final int currentPageNum = Paginator.getPage(request); + LOGGER.log(Level.DEBUG, "Request author articles [authorId={0}, currentPageNum={1}]", authorId, currentPageNum); + + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + + final JSONObject result = userQueryService.getUser(authorId); + if (null == result) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final JSONObject articlesResult = articleQueryService.getArticlesByAuthorId(authorId, currentPageNum, pageSize); + if (null == articlesResult) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final List articles = CollectionUtils.jsonArrayToList(articlesResult.optJSONArray(Keys.RESULTS)); + dataModelService.setArticlesExProperties(context, articles, preference); + final int pageCount = articlesResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + + final Map dataModel = renderer.getDataModel(); + final JSONObject author = result.getJSONObject(User.USER); + prepareShowAuthorArticles(pageNums, dataModel, pageCount, currentPageNum, articles, author); + final HttpServletResponse response = context.getResponse(); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + + statisticMgmtService.incBlogViewCount(context, response); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Shows archive articles with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/archives/{yyyy}/{MM}", method = HttpMethod.GET) + public void showArchiveArticles(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "archive-articles.ftl"); + + try { + final int currentPageNum = Paginator.getPage(request); + final String archiveDateString = context.pathVar("yyyy") + "/" + context.pathVar("MM"); + LOGGER.log(Level.DEBUG, "Request archive date [string={0}, currentPageNum={1}]", archiveDateString, currentPageNum); + final JSONObject result = archiveDateQueryService.getByArchiveDateString(archiveDateString); + if (null == result) { + LOGGER.log(Level.DEBUG, "Can not find articles for the specified archive date[string={0}]", archiveDateString); + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final JSONObject archiveDate = result.getJSONObject(ArchiveDate.ARCHIVE_DATE); + final String archiveDateId = archiveDate.getString(Keys.OBJECT_ID); + + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + + final int articleCount = archiveDateQueryService.getArchiveDatePublishedArticleCount(archiveDateId); + final int pageCount = (int) Math.ceil((double) articleCount / (double) pageSize); + + final List articles = articleQueryService.getArticlesByArchiveDate(archiveDateId, currentPageNum, pageSize); + if (articles.isEmpty()) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + dataModelService.setArticlesExProperties(context, articles, preference); + + final Map dataModel = renderer.getDataModel(); + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + prepareShowArchiveArticles(preference, dataModel, articles, currentPageNum, pageCount, archiveDateString, archiveDate); + final HttpServletResponse response = context.getResponse(); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + + statisticMgmtService.incBlogViewCount(context, response); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Shows an article with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/article", method = HttpMethod.GET) + public void showArticle(final RequestContext context) { + // See PermalinkHandler#dispatchToArticleProcessor() + final JSONObject article = (JSONObject) context.attr(Article.ARTICLE); + if (null == article) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final String articleId = article.optString(Keys.OBJECT_ID); + LOGGER.log(Level.DEBUG, "Article [id={0}]", articleId); + + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "article.ftl"); + + try { + LOGGER.log(Level.TRACE, "Article [title={0}]", article.getString(Article.ARTICLE_TITLE)); + articleQueryService.markdown(article); + + article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.optLong(Article.ARTICLE_CREATED))); + article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED))); + // For + final String metaDescription = Jsoup.parse(article.optString(Article.ARTICLE_ABSTRACT)).text(); + article.put(Article.ARTICLE_ABSTRACT, metaDescription); + final JSONObject preference = optionQueryService.getPreference(); + if (preference.getBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)) { + article.put(Common.HAS_UPDATED, articleQueryService.hasUpdated(article)); + } else { + article.put(Common.HAS_UPDATED, false); + } + + final JSONObject author = articleQueryService.getAuthor(article); + final String authorName = author.getString(User.USER_NAME); + article.put(Common.AUTHOR_NAME, authorName); + final String authorId = author.getString(Keys.OBJECT_ID); + article.put(Common.AUTHOR_ID, authorId); + article.put(Common.AUTHOR_ROLE, author.getString(User.USER_ROLE)); + final String userAvatar = author.optString(UserExt.USER_AVATAR); + article.put(Common.AUTHOR_THUMBNAIL_URL, userAvatar); + dataModelService.fillCategory(article); + final Map dataModel = renderer.getDataModel(); + + prepareShowArticle(preference, dataModel, article); + + final HttpServletResponse response = context.getResponse(); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + + if (!StatisticMgmtService.hasBeenServed(context, response)) { + articleMgmtService.incViewCount(articleId); + } + + statisticMgmtService.incBlogViewCount(context, response); + + // Fire [Before Render Article] event + final JSONObject eventData = new JSONObject(); + eventData.put(Article.ARTICLE, article); + eventManager.fireEventSynchronously(new Event<>(EventTypes.BEFORE_RENDER_ARTICLE, eventData)); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Gets the random articles. + * + * @param preference the specified preference + * @return a list of articles, returns an empty list if not found + */ + private List getRandomArticles(final JSONObject preference) { + try { + final int displayCnt = preference.getInt(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT); + final List ret = articleQueryService.getArticlesRandomly(displayCnt); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + return Collections.emptyList(); + } + } + + /** + * Prepares the specified data model for rendering author articles. + * + * @param pageNums the specified page numbers + * @param dataModel the specified data model + * @param pageCount the specified page count + * @param currentPageNum the specified current page number + * @param articles the specified articles + * @param author the specified author + */ + private void prepareShowAuthorArticles(final List pageNums, + final Map dataModel, + final int pageCount, + final int currentPageNum, + final List articles, + final JSONObject author) { + if (0 != pageNums.size()) { + dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0)); + dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1)); + } + dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + final String previousPageNum = Integer.toString(currentPageNum > 1 ? currentPageNum - 1 : 0); + + dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, "0".equals(previousPageNum) ? "" : previousPageNum); + if (pageCount == currentPageNum + 1) { // The next page is the last page + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, ""); + } else { + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, currentPageNum + 1); + } + + dataModel.put(Article.ARTICLES, articles); + final String authorId = author.optString(Keys.OBJECT_ID); + + dataModel.put(Common.PATH, "/authors/" + authorId); + dataModel.put(Keys.OBJECT_ID, authorId); + + dataModel.put(Common.AUTHOR_NAME, author.optString(User.USER_NAME)); + + final String userAvatar = author.optString(UserExt.USER_AVATAR); + dataModel.put(Common.AUTHOR_THUMBNAIL_URL, userAvatar); + + dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + } + + /** + * Prepares the specified data model for rendering archive articles. + * + * @param preference the specified preference + * @param dataModel the specified data model + * @param articles the specified articles + * @param currentPageNum the specified current page number + * @param pageCount the specified page count + * @param archiveDateString the specified archive data string + * @param archiveDate the specified archive date + * @return page title for caching + */ + private String prepareShowArchiveArticles(final JSONObject preference, + final Map dataModel, + final List articles, + final int currentPageNum, + final int pageCount, + final String archiveDateString, + final JSONObject archiveDate) { + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + + dataModel.put(Article.ARTICLES, articles); + final String previousPageNum = Integer.toString(currentPageNum > 1 ? currentPageNum - 1 : 0); + + dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, "0".equals(previousPageNum) ? "" : previousPageNum); + if (pageCount == currentPageNum + 1) { // The next page is the last page + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, ""); + } else { + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, currentPageNum + 1); + } + dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0)); + dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1)); + dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + dataModel.put(Common.PATH, "/archives/" + archiveDateString); + dataModel.put(Keys.OBJECT_ID, archiveDate.getString(Keys.OBJECT_ID)); + + final long time = archiveDate.getLong(ArchiveDate.ARCHIVE_TIME); + final String dateString = DateFormatUtils.format(time, "yyyy/MM"); + final String[] dateStrings = dateString.split("/"); + final String year = dateStrings[0]; + final String month = dateStrings[1]; + + archiveDate.put(ArchiveDate.ARCHIVE_DATE_YEAR, year); + final String language = Locales.getLanguage(preference.getString(Option.ID_C_LOCALE_STRING)); + String ret; + + if ("en".equals(language)) { + archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH, Dates.EN_MONTHS.get(month)); + ret = Dates.EN_MONTHS.get(month) + " " + year; + } else { + archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH, month); + ret = year + " " + dataModel.get("yearLabel") + " " + month + " " + dataModel.get("monthLabel"); + } + dataModel.put(ArchiveDate.ARCHIVE_DATE, archiveDate); + + return ret; + } + + /** + * Prepares the specified data model for rendering article. + * + * @param preference the specified preference + * @param dataModel the specified data model + * @param article the specified article + * @throws Exception exception + */ + private void prepareShowArticle(final JSONObject preference, final Map dataModel, final JSONObject article) + throws Exception { + article.put(Common.COMMENTABLE, preference.getBoolean(Option.ID_C_COMMENTABLE) && article.getBoolean(Article.ARTICLE_COMMENTABLE)); + article.put(Common.PERMALINK, article.getString(Article.ARTICLE_PERMALINK)); + dataModel.put(Article.ARTICLE, article); + final String articleId = article.getString(Keys.OBJECT_ID); + + Stopwatchs.start("Get Article Sign"); + LOGGER.debug("Getting article sign...."); + final JSONObject sign = articleQueryService.getSign(article.getString(Article.ARTICLE_SIGN_ID), preference); + final String articleTitle = article.optString(Article.ARTICLE_TITLE); + final String author = article.optString(Common.AUTHOR_NAME); + final String url = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK); + String signHtml = sign.optString(Sign.SIGN_HTML); + // 签名档内置模板变量 https://github.com/b3log/solo/issues/12758 + signHtml = StringUtils.replace(signHtml, "{title}", articleTitle); + signHtml = StringUtils.replace(signHtml, "{author}", author); + signHtml = StringUtils.replace(signHtml, "{url}", url); + signHtml = StringUtils.replace(signHtml, "{blog}", Latkes.getServePath()); + sign.put(Sign.SIGN_HTML, signHtml); + article.put(Common.ARTICLE_SIGN, sign); + LOGGER.debug("Got article sign"); + Stopwatchs.end(); + + Stopwatchs.start("Get Next Article"); + LOGGER.debug("Getting the next article...."); + final JSONObject nextArticle = articleQueryService.getNextArticle(articleId); + + if (null != nextArticle) { + dataModel.put(Common.NEXT_ARTICLE_PERMALINK, nextArticle.getString(Article.ARTICLE_PERMALINK)); + dataModel.put(Common.NEXT_ARTICLE_TITLE, nextArticle.getString(Article.ARTICLE_TITLE)); + dataModel.put(Common.NEXT_ARTICLE_ABSTRACT, nextArticle.getString(Article.ARTICLE_ABSTRACT)); + LOGGER.debug("Got the next article"); + } + Stopwatchs.end(); + + Stopwatchs.start("Get Previous Article"); + LOGGER.debug("Getting the previous article...."); + final JSONObject previousArticle = articleQueryService.getPreviousArticle(articleId); + if (null != previousArticle) { + dataModel.put(Common.PREVIOUS_ARTICLE_PERMALINK, previousArticle.getString(Article.ARTICLE_PERMALINK)); + dataModel.put(Common.PREVIOUS_ARTICLE_TITLE, previousArticle.getString(Article.ARTICLE_TITLE)); + dataModel.put(Common.PREVIOUS_ARTICLE_ABSTRACT, previousArticle.getString(Article.ARTICLE_ABSTRACT)); + LOGGER.debug("Got the previous article"); + } + Stopwatchs.end(); + + Stopwatchs.start("Get Article CMTs"); + LOGGER.debug("Getting article's comments...."); + final int cmtCount = article.getInt(Article.ARTICLE_COMMENT_COUNT); + if (0 != cmtCount) { + final List articleComments = commentQueryService.getComments(articleId); + dataModel.put(Article.ARTICLE_COMMENTS_REF, articleComments); + } else { + dataModel.put(Article.ARTICLE_COMMENTS_REF, Collections.emptyList()); + } + LOGGER.debug("Got article's comments"); + Stopwatchs.end(); + + dataModel.put(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT, preference.getInt(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT)); + dataModel.put(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT, preference.getInt(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT)); + dataModel.put(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT, preference.getInt(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT)); + } +} diff --git a/src/main/java/org/b3log/solo/processor/B3Receiver.java b/src/main/java/org/b3log/solo/processor/B3Receiver.java new file mode 100644 index 00000000..808837af --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/B3Receiver.java @@ -0,0 +1,360 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.util.Ids; +import org.b3log.latke.util.Strings; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Comment; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.CommentRepository; +import org.b3log.solo.repository.UserRepository; +import org.b3log.solo.service.*; +import org.json.JSONObject; + +import java.util.Date; + +/** + * Receiving articles and comments from B3log community. Visits B3log 构思 for more details. + * + * @author Liang Ding + * @version 2.0.1.1, Mar 26, 2019 + * @since 0.5.5 + */ +@RequestProcessor +public class B3Receiver { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(B3Receiver.class); + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Comment repository. + */ + @Inject + private static CommentRepository commentRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Comment management service. + */ + @Inject + private CommentMgmtService commentMgmtService; + + /** + * Article management service. + */ + @Inject + private ArticleMgmtService articleMgmtService; + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Adds or updates an article with the specified request. + *

+ * Request json: + *

+     * {
+     *     "article": {
+     *         "id": "",
+     *          "title": "",
+     *          "content": "",
+     *          "contentHTML": "",
+     *          "tags": "tag1,tag2,tag3"
+     *     },
+     *     "client": {
+     *         "userName": "",
+     *         "userB3Key": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "oId": "", // Generated article id
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @RequestProcessing(value = "/apis/symphony/article", method = {HttpMethod.POST, HttpMethod.PUT}) + public void postArticle(final RequestContext context) { + final JSONObject ret = new JSONObject().put(Keys.CODE, 0); + context.renderJSON(ret); + + final JSONObject requestJSONObject = context.requestJSON(); + LOGGER.log(Level.INFO, "Adds an article from Sym [" + requestJSONObject.toString() + "]"); + + try { + final JSONObject client = requestJSONObject.optJSONObject("client"); + final String articleAuthorName = client.optString(User.USER_NAME); + final JSONObject articleAuthor = userRepository.getByUserName(articleAuthorName); + if (null == articleAuthor) { + ret.put(Keys.CODE, 1); + final String msg = "Not found user [" + articleAuthorName + "]"; + ret.put(Keys.MSG, msg); + LOGGER.log(Level.WARN, msg); + + return; + } + + final String b3Key = client.optString(UserExt.USER_B3_KEY); + final String key = articleAuthor.optString(UserExt.USER_B3_KEY); + if (!StringUtils.equals(key, b3Key)) { + ret.put(Keys.CODE, 1); + final String msg = "Wrong key"; + ret.put(Keys.MSG, msg); + LOGGER.log(Level.WARN, msg); + + return; + } + + final JSONObject symArticle = requestJSONObject.optJSONObject(Article.ARTICLE); + final String title = symArticle.optString("title"); + final String articleId = symArticle.optString("id"); + final JSONObject oldArticle = articleQueryService.getArticleById(articleId); + if (null == oldArticle) { + final JSONObject article = new JSONObject(). + put(Keys.OBJECT_ID, symArticle.optString("id")). + put(Article.ARTICLE_TITLE, title). + put(Article.ARTICLE_CONTENT, symArticle.optString("content")). + put(Article.ARTICLE_TAGS_REF, symArticle.optString("tags")); + article.put(Article.ARTICLE_AUTHOR_ID, articleAuthor.getString(Keys.OBJECT_ID)); + final String articleContent = article.optString(Article.ARTICLE_CONTENT); + article.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(articleContent)); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, false); // Do not send to rhythm + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + final String content = article.getString(Article.ARTICLE_CONTENT); + article.put(Article.ARTICLE_CONTENT, content); + final JSONObject addRequest = new JSONObject().put(Article.ARTICLE, article); + articleMgmtService.addArticle(addRequest); + LOGGER.log(Level.INFO, "Added an article [" + title + "] via Sym"); + + return; + } + + final String articleContent = symArticle.optString("content"); + oldArticle.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(articleContent)); + oldArticle.put(Article.ARTICLE_CONTENT, articleContent); + oldArticle.put(Article.ARTICLE_TITLE, symArticle.optString("title")); + oldArticle.put(Article.ARTICLE_TAGS_REF, symArticle.optString("tags")); + oldArticle.put(Common.POST_TO_COMMUNITY, false); // Do not send to rhythm + final JSONObject updateRequest = new JSONObject().put(Article.ARTICLE, oldArticle); + articleMgmtService.updateArticle(updateRequest); + LOGGER.log(Level.INFO, "Updated an article [" + title + "] via Sym"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + ret.put(Keys.CODE, 1).put(Keys.MSG, e.getMessage()); + } + } + + /** + * Adds a comment with the specified request. + *

+ * Request json: + *

+     * {
+     *     "comment": {
+     *         "articleId": "",
+     *         "content": "",
+     *         "contentHTML": "",
+     *         "ua": "",
+     *         "ip": "",
+     *         "authorName": "",
+     *         "authorURL": "",
+     *         "authorAvatarURL": "",
+     *         "isArticleAuthor": true,
+     *         "time": 1457784330398
+     *     },
+     *     "client": {
+     *         "userName": "88250",
+     *         "userB3Key": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": true
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @RequestProcessing(value = "/apis/symphony/comment", method = HttpMethod.PUT) + public void addComment(final RequestContext context) { + final JSONObject ret = new JSONObject().put(Keys.CODE, 0); + context.renderJSON(ret); + + final JSONObject requestJSONObject = context.requestJSON(); + + LOGGER.log(Level.INFO, "Adds a comment from Sym [" + requestJSONObject.toString() + "]"); + + try { + final JSONObject symCmt = requestJSONObject.optJSONObject(Comment.COMMENT); + final JSONObject symClient = requestJSONObject.optJSONObject("client"); + final String articleAuthorName = symClient.optString(User.USER_NAME); + final JSONObject articleAuthor = userRepository.getByUserName(articleAuthorName); + if (null == articleAuthor) { + ret.put(Keys.CODE, 1); + final String msg = "Not found user [" + articleAuthorName + "]"; + ret.put(Keys.MSG, msg); + LOGGER.log(Level.WARN, msg); + + return; + } + + final String b3Key = symClient.optString(UserExt.USER_B3_KEY); + final String key = articleAuthor.optString(UserExt.USER_B3_KEY); + if (!StringUtils.equals(key, b3Key)) { + ret.put(Keys.CODE, 1); + final String msg = "Wrong key"; + ret.put(Keys.MSG, msg); + LOGGER.log(Level.WARN, msg); + + return; + } + + final String articleId = symCmt.getString("articleId"); + final JSONObject article = articleRepository.get(articleId); + if (null == article) { + ret.put(Keys.CODE, 1); + final String msg = "Not found the specified article [id=" + articleId + "]"; + ret.put(Keys.MSG, msg); + LOGGER.log(Level.WARN, msg); + + return; + } + + final String commentName = symCmt.getString("authorName"); + String commentURL = symCmt.optString("authorURL"); + if (!Strings.isURL(commentURL)) { + commentURL = ""; + } + final String commentThumbnailURL = symCmt.getString("authorAvatarURL"); + + final JSONObject commenter = userRepository.getByUserName(commentName); + if (null == commenter) { + // 社区回帖同步博客评论 https://github.com/b3log/solo/issues/12691 + final JSONObject addUserReq = new JSONObject(); + addUserReq.put(User.USER_NAME, commentName); + addUserReq.put(UserExt.USER_AVATAR, commentThumbnailURL); + addUserReq.put(User.USER_ROLE, Role.VISITOR_ROLE); + addUserReq.put(UserExt.USER_GITHUB_ID, ""); + addUserReq.put(UserExt.USER_B3_KEY, ""); + try { + userMgmtService.addUser(addUserReq); + LOGGER.log(Level.INFO, "Created a user [role=" + Role.VISITOR_ROLE + "] via Sym comment"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Adds a user [" + commentName + "] failed", e); + ret.put(Keys.CODE, 1); + ret.put(Keys.MSG, "Adds a user [" + commentName + "] failed"); + + return; + } + } + + if (!optionQueryService.allowComment() || !article.optBoolean(Article.ARTICLE_COMMENTABLE)) { + ret.put(Keys.CODE, 1); + final String msg = "Not allow comment"; + ret.put(Keys.MSG, msg); + LOGGER.log(Level.WARN, msg); + + return; + } + + String commentContent = symCmt.getString("content"); // Markdown + + final Transaction transaction = commentRepository.beginTransaction(); + final JSONObject comment = new JSONObject(); + final String commentId = Ids.genTimeMillisId(); + comment.put(Keys.OBJECT_ID, commentId); + comment.put(Comment.COMMENT_NAME, commentName); + comment.put(Comment.COMMENT_URL, commentURL); + comment.put(Comment.COMMENT_THUMBNAIL_URL, commentThumbnailURL); + comment.put(Comment.COMMENT_CONTENT, commentContent); + final Date date = new Date(); + comment.put(Comment.COMMENT_CREATED, date.getTime()); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, ""); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, ""); + comment.put(Comment.COMMENT_ON_ID, articleId); + final String commentSharpURL = Comment.getCommentSharpURLForArticle(article, commentId); + comment.put(Comment.COMMENT_SHARP_URL, commentSharpURL); + commentRepository.add(comment); + articleMgmtService.incArticleCommentCount(articleId); + transaction.commit(); + + LOGGER.log(Level.INFO, "Added a comment from Sym [" + requestJSONObject.toString() + "]"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + ret.put(Keys.CODE, 1).put(Keys.MSG, e.getMessage()); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/BlogProcessor.java b/src/main/java/org/b3log/solo/processor/BlogProcessor.java new file mode 100644 index 00000000..cbeaa66c --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/BlogProcessor.java @@ -0,0 +1,185 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.User; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.*; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Blog processor. + * + * @author Liang Ding + * @version 1.3.1.8, Feb 7, 2019 + * @since 0.4.6 + */ +@RequestProcessor +public class BlogProcessor { + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * Statistic query service. + */ + @Inject + private StatisticQueryService statisticQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Gets blog information. + *
    + *
  • Time of the recent updated article
  • + *
  • Article count
  • + *
  • Comment count
  • + *
  • Tag count
  • + *
  • Serve path
  • + *
  • Static serve path
  • + *
  • Solo version
  • + *
  • Runtime mode
  • + *
  • Runtime database
  • + *
  • Locale
  • + *
  • Admin username
  • + *
+ * + * @param context the specified context + */ + @RequestProcessing(value = "/blog/info", method = HttpMethod.GET) + public void getBlogInfo(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + jsonObject.put("recentArticleTime", articleQueryService.getRecentArticleTime()); + final JSONObject statistic = statisticQueryService.getStatistic(); + jsonObject.put("articleCount", statistic.getLong(Option.ID_T_STATISTIC_PUBLISHED_ARTICLE_COUNT)); + jsonObject.put("commentCount", statistic.getLong(Option.ID_T_STATISTIC_PUBLISHED_BLOG_COMMENT_COUNT)); + jsonObject.put("tagCount", tagQueryService.getTagCount()); + jsonObject.put("servePath", Latkes.getServePath()); + jsonObject.put("staticServePath", Latkes.getStaticServePath()); + jsonObject.put("version", SoloServletListener.VERSION); + jsonObject.put("runtimeMode", Latkes.getRuntimeMode()); + jsonObject.put("runtimeDatabase", Latkes.getRuntimeDatabase()); + jsonObject.put("locale", Latkes.getLocale()); + String userName = ""; + try { + userName = userQueryService.getAdmin().optString(User.USER_NAME); + } catch (final Exception e) { + // ignored + } + jsonObject.put("userName", userName); + } + + /** + * Gets tags of all articles. + *

+ *

+     * {
+     *     "data": [
+     *         ["tag1", "tag2", ....], // tags of one article
+     *         ["tagX", "tagY", ....], // tags of another article
+     *         ....
+     *     ]
+     * }
+     * 
+ *

+ * + * @param context the specified context + */ + @RequestProcessing(value = "/blog/articles-tags", method = HttpMethod.GET) + public void getArticlesTags(final RequestContext context) { + final JSONObject requestJSONObject = new JSONObject(); + requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, 1); + requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, Integer.MAX_VALUE); + requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, Integer.MAX_VALUE); + requestJSONObject.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + + final JSONArray excludes = new JSONArray(); + + excludes.put(Article.ARTICLE_CONTENT); + excludes.put(Article.ARTICLE_UPDATED); + excludes.put(Article.ARTICLE_CREATED); + excludes.put(Article.ARTICLE_AUTHOR_ID); + excludes.put(Article.ARTICLE_RANDOM_DOUBLE); + + requestJSONObject.put(Keys.EXCLUDES, excludes); + + final JSONObject result = articleQueryService.getArticles(requestJSONObject); + final JSONArray articles = result.optJSONArray(Article.ARTICLES); + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + final JSONArray data = new JSONArray(); + ret.put("data", data); + + for (int i = 0; i < articles.length(); i++) { + final JSONObject article = articles.optJSONObject(i); + final String tagString = article.optString(Article.ARTICLE_TAGS_REF); + + final JSONArray tagArray = new JSONArray(); + data.put(tagArray); + + final String[] tags = tagString.split(","); + + for (final String tag : tags) { + final String trim = tag.trim(); + if (StringUtils.isNotBlank(trim)) { + tagArray.put(tag); + } + } + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/CategoryProcessor.java b/src/main/java/org/b3log/solo/processor/CategoryProcessor.java new file mode 100644 index 00000000..2a59409e --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/CategoryProcessor.java @@ -0,0 +1,244 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.latke.util.Paginator; +import org.b3log.latke.util.Stopwatchs; +import org.b3log.latke.util.URLs; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.*; +import org.b3log.solo.util.Skins; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * Category processor. + * + * @author Liang Ding + * @version 1.1.0.0, Mar 30, 2019 + * @since 2.0.0 + */ +@RequestProcessor +public class CategoryProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CategoryProcessor.class); + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Category query service. + */ + @Inject + private CategoryQueryService categoryQueryService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Gets category articles paged with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/articles/category/{categoryURI}", method = HttpMethod.GET) + public void getCategoryArticlesByPage(final RequestContext context) { + final JSONObject jsonObject = new JSONObject(); + + final HttpServletRequest request = context.getRequest(); + final String categoryURI = context.pathVar("categoryURI"); + final int currentPageNum = Paginator.getPage(request); + + Stopwatchs.start("Get Category-Articles Paged [categoryURI=" + categoryURI + ", pageNum=" + currentPageNum + ']'); + try { + final JSONObject category = categoryQueryService.getByURI(categoryURI); + if (null == category) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + jsonObject.put(Keys.STATUS_CODE, true); + final String categoryId = category.optString(Keys.OBJECT_ID); + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final JSONObject articlesResult = articleQueryService.getCategoryArticles(categoryId, currentPageNum, pageSize); + final List articles = (List) articlesResult.opt(Keys.RESULTS); + dataModelService.setArticlesExProperties(context, articles, preference); + final int pageCount = articlesResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + + final JSONObject result = new JSONObject(); + final JSONObject pagination = new JSONObject(); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + result.put(Pagination.PAGINATION, pagination); + result.put(Article.ARTICLES, articles); + jsonObject.put(Keys.RESULTS, result); + } catch (final Exception e) { + jsonObject.put(Keys.STATUS_CODE, false); + LOGGER.log(Level.ERROR, "Gets article paged failed", e); + } finally { + Stopwatchs.end(); + } + + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + } + + /** + * Shows articles related with a category with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/category/{categoryURI}", method = HttpMethod.GET) + public void showCategoryArticles(final RequestContext context) { + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "category-articles.ftl"); + final Map dataModel = renderer.getDataModel(); + + final HttpServletRequest request = context.getRequest(); + final HttpServletResponse response = context.getResponse(); + + try { + String categoryURI = context.pathVar("categoryURI"); + categoryURI = URLs.encode(categoryURI); + final int currentPageNum = Paginator.getPage(request); + LOGGER.log(Level.DEBUG, "Category [URI={0}, currentPageNum={1}]", categoryURI, currentPageNum); + final JSONObject category = categoryQueryService.getByURI(categoryURI); + if (null == category) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + dataModel.put(Category.CATEGORY, category); + + final JSONObject preference = optionQueryService.getPreference(); + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final String categoryId = category.optString(Keys.OBJECT_ID); + + final JSONObject result = articleQueryService.getCategoryArticles(categoryId, currentPageNum, pageSize); + final List articles = (List) result.opt(Keys.RESULTS); + + final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + if (0 == pageCount) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + dataModelService.setArticlesExProperties(context, articles, preference); + + final List pageNums = (List) result.optJSONObject(Pagination.PAGINATION).opt(Pagination.PAGINATION_PAGE_NUMS); + fillPagination(dataModel, pageCount, currentPageNum, articles, pageNums); + dataModel.put(Common.PATH, "/category/" + URLs.encode(categoryURI)); + + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + + statisticMgmtService.incBlogViewCount(context, response); + } catch (final ServiceException | JSONException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Fills pagination. + * + * @param dataModel the specified data model + * @param pageCount the specified page count + * @param currentPageNum the specified current page number + * @param articles the specified articles + * @param pageNums the specified page numbers + */ + private void fillPagination(final Map dataModel, + final int pageCount, final int currentPageNum, + final List articles, + final List pageNums) { + final String previousPageNum = Integer.toString(currentPageNum > 1 ? currentPageNum - 1 : 0); + + dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, "0".equals(previousPageNum) ? "" : previousPageNum); + if (pageCount == currentPageNum + 1) { // The next page is the last page + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, ""); + } else { + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, currentPageNum + 1); + } + dataModel.put(Article.ARTICLES, articles); + dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0)); + dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1)); + dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + } +} diff --git a/src/main/java/org/b3log/solo/processor/CommentProcessor.java b/src/main/java/org/b3log/solo/processor/CommentProcessor.java new file mode 100644 index 00000000..4cece6ae --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/CommentProcessor.java @@ -0,0 +1,217 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import freemarker.template.Template; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.User; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Comment; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.CommentMgmtService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.UserMgmtService; +import org.b3log.solo.service.UserQueryService; +import org.b3log.solo.util.Skins; +import org.b3log.solo.util.Solos; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * Comment processor. + * + * @author Liang Ding + * @author ArmstrongCN + * @version 1.4.0.0, Apr 18, 2019 + * @since 0.3.1 + */ +@RequestProcessor +public class CommentProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CommentProcessor.class); + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Comment management service. + */ + @Inject + private CommentMgmtService commentMgmtService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Adds a comment to an article. + * + *

+ * Request json: + *

+     * {
+     *     "captcha": "",
+     *     "oId": articleId,
+     *     "commentName": "",
+     *     "commentURL": "",
+     *     "commentContent": "",
+     *     "commentOriginalCommentId": "" // optional, if exists this key, the comment is an reply
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "oId": generatedCommentId,
+     *     "sc": "COMMENT_ARTICLE_SUCC",
+     *     "commentDate": "", // yyyy/MM/dd HH:mm:ss
+     *     "commentSharpURL": "",
+     *     "commentThumbnailURL": "",
+     *     "commentOriginalCommentName": "", // if exists this key, the comment is an reply
+     *     "commentContent": ""
+     * }
+     * 
+ *

+ * + * @param context the specified context, including a request json object + */ + @RequestProcessing(value = "/article/comments", method = HttpMethod.POST) + public void addArticleComment(final RequestContext context) { + final JSONObject requestJSONObject = context.requestJSON(); + requestJSONObject.put(Common.TYPE, Article.ARTICLE); + + String anonymousName = "不愿透露姓名的靓仔"; + String commentName = ""; + try { + commentName = requestJSONObject.getString("commentName"); + } catch (JSONException JSONE) { + requestJSONObject.put("commentName", anonymousName); + commentName = anonymousName; + } + + fillCommenter(requestJSONObject, context); + + final JSONObject jsonObject = commentMgmtService.checkAddCommentRequest(requestJSONObject); + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + renderer.setJSONObject(jsonObject); + + /*if (!jsonObject.optBoolean(Keys.STATUS_CODE)) { + LOGGER.log(Level.WARN, "Can't add comment[msg={0}]", jsonObject.optString(Keys.MSG)); + return; + }*/ + + //防止冒用用户名 + if (!commentName.equals(anonymousName)) { + if (!Solos.isLoggedIn(context)) { + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, "Need login"); + + return; + } + } + + try { + final JSONObject addResult = commentMgmtService.addArticleComment(requestJSONObject); + + final Map dataModel = new HashMap<>(); + dataModel.put(Comment.COMMENT, addResult); + final JSONObject article = addResult.optJSONObject(Article.ARTICLE); + article.put(Common.COMMENTABLE, addResult.opt(Common.COMMENTABLE)); + article.put(Common.PERMALINK, addResult.opt(Common.PERMALINK)); + dataModel.put(Article.ARTICLE, article); + + // 添加评论优化 https://github.com/b3log/solo/issues/12246 + try { + final String skinDirName = (String) context.attr(Keys.TEMAPLTE_DIR_NAME); + final Template template = Skins.getSkinTemplate(context, "common-comment.ftl"); + final JSONObject preference = optionQueryService.getPreference(); + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), skinDirName, dataModel); + Keys.fillServer(dataModel); + final StringWriter stringWriter = new StringWriter(); + template.process(dataModel, stringWriter); + stringWriter.close(); + final String cmtTpl = stringWriter.toString(); + + addResult.put("cmtTpl", cmtTpl); + } catch (final Exception e) { + // 1.9.0 向后兼容 + } + + addResult.put(Keys.STATUS_CODE, true); + + renderer.setJSONObject(addResult); + } catch (final Exception e) { + + LOGGER.log(Level.ERROR, "Can not add comment on article", e); + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("addFailLabel")); + } + } + + /** + * Fills commenter info if logged in. + * + * @param requestJSONObject the specified request json object + * @param context the specified HTTP servlet request context + */ + private void fillCommenter(final JSONObject requestJSONObject, final RequestContext context) { + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + if (null == currentUser) { + return; + } + + requestJSONObject.put(Comment.COMMENT_NAME, currentUser.optString(User.USER_NAME)); + requestJSONObject.put(Comment.COMMENT_URL, currentUser.optString(User.USER_URL)); + } +} diff --git a/src/main/java/org/b3log/solo/processor/ErrorProcessor.java b/src/main/java/org/b3log/solo/processor/ErrorProcessor.java new file mode 100644 index 00000000..afff6c78 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/ErrorProcessor.java @@ -0,0 +1,120 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.util.Locales; +import org.b3log.solo.model.Common; +import org.b3log.solo.service.DataModelService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.UserQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * Error processor. + * + * @author Liang Ding + * @version 1.0.2.0, Mar 30, 2019 + * @since 0.4.5 + */ +@RequestProcessor +public class ErrorProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleProcessor.class); + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Handles the error. + * + * @param context the specified context + * @throws Exception exception + */ + @RequestProcessing(value = "/error/{statusCode}", method = {HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE}) + public void showErrorPage(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final String statusCode = context.pathVar("statusCode"); + if (StringUtils.equals("GET", context.method())) { + final String requestURI = context.requestURI(); + final String templateName = statusCode + ".ftl"; + LOGGER.log(Level.TRACE, "Shows error page [requestURI={0}, templateName={1}]", requestURI, templateName); + + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "error/" + templateName); + final Map dataModel = renderer.getDataModel(); + try { + final Map langs = langPropsService.getAll(Locales.getLocale(request)); + dataModel.putAll(langs); + final JSONObject preference = optionQueryService.getPreference(); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + final String msg = (String) context.attr(Keys.MSG); + dataModel.put(Keys.MSG, msg); + dataModel.put(Common.LOGIN_URL, userQueryService.getLoginURL(Common.ADMIN_INDEX_URI)); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Shows error page failed", e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + + Solos.addGoogleNoIndex(context); + } else { + context.renderJSON().renderMsg(statusCode); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/FeedProcessor.java b/src/main/java/org/b3log/solo/processor/FeedProcessor.java new file mode 100644 index 00000000..7c004085 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/FeedProcessor.java @@ -0,0 +1,251 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import com.vdurmont.emoji.EmojiParser; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.*; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AtomRenderer; +import org.b3log.latke.servlet.renderer.RssRenderer; +import org.b3log.latke.util.Locales; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.atom.Category; +import org.b3log.solo.model.atom.Entry; +import org.b3log.solo.model.atom.Feed; +import org.b3log.solo.model.rss.Channel; +import org.b3log.solo.model.rss.Item; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.service.ArticleQueryService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.util.Markdowns; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Feed (Atom/RSS) processor. + * + * @author Liang Ding + * @author feroozkhanchintu + * @author nanolikeyou + * @version 2.0.0.3, Jul 29, 2019 + * @since 0.3.1 + */ +@RequestProcessor +public class FeedProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(FeedProcessor.class); + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Blog articles Atom output. + * + * @param context the specified context + */ + @RequestProcessing(value = "/atom.xml", method = {HttpMethod.GET, HttpMethod.HEAD}) + public void blogArticlesAtom(final RequestContext context) { + final AtomRenderer renderer = new AtomRenderer(); + context.setRenderer(renderer); + + final Feed feed = new Feed(); + try { + final JSONObject preference = optionQueryService.getPreference(); + final String blogTitle = preference.getString(Option.ID_C_BLOG_TITLE); + final String blogSubtitle = preference.getString(Option.ID_C_BLOG_SUBTITLE); + final int outputCnt = preference.getInt(Option.ID_C_FEED_OUTPUT_CNT); + feed.setTitle(blogTitle); + feed.setSubtitle(blogSubtitle); + feed.setUpdated(new Date()); + feed.setAuthor(blogTitle); + feed.setLink(Latkes.getServePath() + "/atom.xml"); + feed.setId(Latkes.getServePath() + "/"); + + final List filters = new ArrayList<>(); + filters.add(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)); + filters.add(new PropertyFilter(Article.ARTICLE_VIEW_PWD, FilterOperator.EQUAL, "")); + final Query query = new Query().setPage(1, outputCnt). + setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters)). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING).setPageCount(1); + final JSONObject articleResult = articleRepository.get(query); + final JSONArray articles = articleResult.getJSONArray(Keys.RESULTS); + final boolean isFullContent = "fullContent".equals(preference.getString(Option.ID_C_FEED_OUTPUT_MODE)); + for (int i = 0; i < articles.length(); i++) { + final Entry entry = getEntry(articles, isFullContent, i); + feed.addEntry(entry); + } + + renderer.setContent(feed.toString()); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Get blog article feed error", e); + + context.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + } + } + + private Entry getEntry(final JSONArray articles, final boolean isFullContent, int i) + throws JSONException, ServiceException { + final JSONObject article = articles.getJSONObject(i); + final Entry ret = new Entry(); + final String title = article.getString(Article.ARTICLE_TITLE); + ret.setTitle(title); + final String summary = isFullContent ? article.getString(Article.ARTICLE_CONTENT) + : article.optString(Article.ARTICLE_ABSTRACT); + ret.setSummary(summary); + final long updated = article.getLong(Article.ARTICLE_UPDATED); + ret.setUpdated(new Date(updated)); + final String link = Latkes.getServePath() + article.getString(Article.ARTICLE_PERMALINK); + ret.setLink(link); + ret.setId(link); + final String authorName = articleQueryService.getAuthor(article).getString(User.USER_NAME); + ret.setAuthor(authorName); + final String tagsString = article.getString(Article.ARTICLE_TAGS_REF); + final String[] tagStrings = tagsString.split(","); + for (final String tagString : tagStrings) { + final Category catetory = new Category(); + ret.addCatetory(catetory); + catetory.setTerm(tagString); + } + + return ret; + } + + /** + * Blog articles RSS output. + * + * @param context the specified context + * @throws Exception exception + */ + @RequestProcessing(value = "/rss.xml", method = {HttpMethod.GET, HttpMethod.HEAD}) + public void blogArticlesRSS(final RequestContext context) { + final RssRenderer renderer = new RssRenderer(); + context.setRenderer(renderer); + + final Channel channel = new Channel(); + + try { + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final String blogTitle = preference.getString(Option.ID_C_BLOG_TITLE); + final String blogSubtitle = preference.getString(Option.ID_C_BLOG_SUBTITLE); + final int outputCnt = preference.getInt(Option.ID_C_FEED_OUTPUT_CNT); + + channel.setTitle(blogTitle); + channel.setLastBuildDate(new Date()); + channel.setLink(Latkes.getServePath()); + channel.setAtomLink(Latkes.getServePath() + "/rss.xml"); + channel.setGenerator("Solo, v" + SoloServletListener.VERSION + ", https://solo.b3log.org"); + final String localeString = preference.getString(Option.ID_C_LOCALE_STRING); + final String country = Locales.getCountry(localeString).toLowerCase(); + final String language = Locales.getLanguage(localeString).toLowerCase(); + channel.setLanguage(language + '-' + country); + channel.setDescription(blogSubtitle); + + final List filters = new ArrayList<>(); + filters.add(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)); + filters.add(new PropertyFilter(Article.ARTICLE_VIEW_PWD, FilterOperator.EQUAL, "")); + final Query query = new Query().setPageCount(1).setPage(1, outputCnt). + setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters)). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING); + final JSONObject articleResult = articleRepository.get(query); + final JSONArray articles = articleResult.getJSONArray(Keys.RESULTS); + final boolean isFullContent = "fullContent".equals(preference.getString(Option.ID_C_FEED_OUTPUT_MODE)); + for (int i = 0; i < articles.length(); i++) { + final Item item = getItem(articles, isFullContent, i); + channel.addItem(item); + } + + renderer.setContent(channel.toString()); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Get blog article rss error", e); + + context.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + } + } + + private Item getItem(final JSONArray articles, final boolean isFullContent, int i) throws JSONException, ServiceException { + final JSONObject article = articles.getJSONObject(i); + final Item ret = new Item(); + String title = article.getString(Article.ARTICLE_TITLE); + title = EmojiParser.parseToAliases(title); + ret.setTitle(title); + String description = isFullContent + ? article.getString(Article.ARTICLE_CONTENT) + : article.optString(Article.ARTICLE_ABSTRACT); + description = EmojiParser.parseToAliases(description); + description = Markdowns.toHTML(description); + ret.setDescription(description); + final long pubDate = article.getLong(Article.ARTICLE_UPDATED); + ret.setPubDate(new Date(pubDate)); + final String link = Latkes.getServePath() + article.getString(Article.ARTICLE_PERMALINK); + ret.setLink(link); + ret.setGUID(link); + final String authorName = articleQueryService.getAuthor(article).getString(User.USER_NAME); + ret.setAuthor(authorName); + final String tagsString = article.getString(Article.ARTICLE_TAGS_REF); + final String[] tagStrings = tagsString.split(","); + for (final String tagString : tagStrings) { + final org.b3log.solo.model.rss.Category catetory = new org.b3log.solo.model.rss.Category(); + ret.addCatetory(catetory); + catetory.setTerm(tagString); + } + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/processor/IndexProcessor.java b/src/main/java/org/b3log/solo/processor/IndexProcessor.java new file mode 100644 index 00000000..fef8e743 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/IndexProcessor.java @@ -0,0 +1,247 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.util.Locales; +import org.b3log.latke.util.Paginator; +import org.b3log.latke.util.URLs; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.DataModelService; +import org.b3log.solo.service.InitService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.StatisticMgmtService; +import org.b3log.solo.util.Skins; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Calendar; +import java.util.Map; + +/** + * Index processor. + * + * @author Liang Ding + * @author DASHU + * @author Vanessa + * @version 1.2.4.17, Jul 17, 2019 + * @since 0.3.1 + */ +@RequestProcessor +public class IndexProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(IndexProcessor.class); + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Initialization service. + */ + @Inject + private InitService initService; + + /** + * Shows index with the specified context. + * + * @param context the specified context + * @throws Exception exception + */ + @RequestProcessing(value = {"", "/"}, method = HttpMethod.GET) + public void showIndex(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final HttpServletResponse response = context.getResponse(); + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "index.ftl"); + final Map dataModel = renderer.getDataModel(); + try { + final int currentPageNum = Paginator.getPage(request); + final JSONObject preference = optionQueryService.getPreference(); + + // 前台皮肤切换 https://github.com/b3log/solo/issues/12060 + String specifiedSkin = Skins.getSkinDirName(context); + if (StringUtils.isBlank(specifiedSkin)) { + final JSONObject skinOpt = optionQueryService.getSkin(); + specifiedSkin = Solos.isMobile(request) ? + skinOpt.optString(Option.ID_C_MOBILE_SKIN_DIR_NAME) : + skinOpt.optString(Option.ID_C_SKIN_DIR_NAME); + } + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, specifiedSkin); + + Cookie cookie; + if (!Solos.isMobile(request)) { + cookie = new Cookie(Common.COOKIE_NAME_SKIN, specifiedSkin); + } else { + cookie = new Cookie(Common.COOKIE_NAME_MOBILE_SKIN, specifiedSkin); + } + cookie.setMaxAge(60 * 60); // 1 hour + cookie.setPath("/"); + response.addCookie(cookie); + + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + + dataModelService.fillIndexArticles(context, dataModel, currentPageNum, preference); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + + dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + final int previousPageNum = currentPageNum > 1 ? currentPageNum - 1 : 0; + dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, previousPageNum); + + final Integer pageCount = (Integer) dataModel.get(Pagination.PAGINATION_PAGE_COUNT); + final int nextPageNum = currentPageNum + 1 > pageCount ? pageCount : currentPageNum + 1; + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, nextPageNum); + dataModel.put(Common.PATH, ""); + + statisticMgmtService.incBlogViewCount(context, response); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Shows start page. + * + * @param context the specified context + */ + @RequestProcessing(value = "/start", method = HttpMethod.GET) + public void showStart(final RequestContext context) { + if (initService.isInited() && null != Solos.getCurrentUser(context.getRequest(), context.getResponse())) { + context.sendRedirect(Latkes.getServePath()); + + return; + } + + String referer = context.header("referer"); + if (StringUtils.isBlank(referer) || !isInternalLinks(referer)) { + referer = Latkes.getServePath(); + } + + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "common-template/start.ftl"); + final Map dataModel = renderer.getDataModel(); + final HttpServletRequest request = context.getRequest(); + final Map langs = langPropsService.getAll(Locales.getLocale(request)); + dataModel.putAll(langs); + dataModel.put(Common.VERSION, SoloServletListener.VERSION); + dataModel.put(Common.STATIC_RESOURCE_VERSION, Latkes.getStaticResourceVersion()); + dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR))); + dataModel.put(Common.REFERER, URLs.encode(referer)); + Keys.fillRuntime(dataModel); + dataModelService.fillMinified(dataModel); + dataModelService.fillFaviconURL(dataModel, optionQueryService.getPreference()); + dataModelService.fillUsite(dataModel); + Solos.addGoogleNoIndex(context); + } + + /** + * Logout. + * + * @param context the specified context + */ + @RequestProcessing(value = "/logout", method = HttpMethod.GET) + public void logout(final RequestContext context) { + final HttpServletRequest httpServletRequest = context.getRequest(); + + Solos.logout(httpServletRequest, context.getResponse()); + + Solos.addGoogleNoIndex(context); + context.sendRedirect(Latkes.getServePath()); + } + + /** + * Shows kill browser page with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/kill-browser", method = HttpMethod.GET) + public void showKillBrowser(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "common-template/kill-browser.ftl"); + final Map dataModel = renderer.getDataModel(); + try { + final Map langs = langPropsService.getAll(Locales.getLocale(request)); + dataModel.putAll(langs); + final JSONObject preference = optionQueryService.getPreference(); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + Keys.fillServer(dataModel); + Keys.fillRuntime(dataModel); + dataModelService.fillMinified(dataModel); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Preventing unvalidated redirects and forwards. See more at: + * https://www.owasp.org/index.php/ + * Unvalidated_Redirects_and_Forwards_Cheat_Sheet. + * + * @return whether the destinationURL is an internal link + */ + private boolean isInternalLinks(final String destinationURL) { + return destinationURL.startsWith(Latkes.getServePath()); + } +} diff --git a/src/main/java/org/b3log/solo/processor/InitCheckHandler.java b/src/main/java/org/b3log/solo/processor/InitCheckHandler.java new file mode 100644 index 00000000..548a8da9 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/InitCheckHandler.java @@ -0,0 +1,87 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.handler.Handler; +import org.b3log.solo.service.InitService; + +import javax.servlet.http.HttpServletResponse; + +/** + * Checks initialization handler. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 1, 2019 + * @since 3.2.0 + */ +public class InitCheckHandler implements Handler { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(InitCheckHandler.class); + + /** + * Whether initialization info reported. + */ + private static boolean initReported; + + @Override + public void handle(final RequestContext context) { + final String requestURI = context.requestURI(); + final boolean isSpiderBot = (boolean) context.attr(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT); + LOGGER.log(Level.TRACE, "Request [URI={0}]", requestURI); + + // 禁止直接获取 robots.txt https://github.com/b3log/solo/issues/12543 + if (requestURI.startsWith("/robots.txt") && !isSpiderBot) { + context.sendError(HttpServletResponse.SC_FORBIDDEN); + + return; + } + + final BeanManager beanManager = BeanManager.getInstance(); + final InitService initService = beanManager.getReference(InitService.class); + if (initService.isInited()) { + context.handle(); + + return; + } + + if (StringUtils.startsWith(requestURI, Latkes.getContextPath() + "/oauth/github")) { + // Do initialization + context.handle(); + + return; + } + + if (!initReported) { + LOGGER.log(Level.DEBUG, "Solo has not been initialized, so redirects to /start"); + initReported = true; + } + + context.attr(Keys.HttpRequest.REQUEST_URI, Latkes.getContextPath() + "/start"); + context.handle(); + } +} diff --git a/src/main/java/org/b3log/solo/processor/KanBanNiangProcessor.java b/src/main/java/org/b3log/solo/processor/KanBanNiangProcessor.java new file mode 100644 index 00000000..b704ccc2 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/KanBanNiangProcessor.java @@ -0,0 +1,90 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.math.RandomUtils; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.SoloServletListener; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.ServletContext; +import java.io.InputStream; + +/** + * KanBanNiang processor. https://github.com/b3log/solo/issues/12472 + * + * @author Liang Ding + * @version 1.0.0.2, Mar 4, 2019 + * @since 2.9.2 + */ +@RequestProcessor +public class KanBanNiangProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(KanBanNiangProcessor.class); + + /** + * Returns a random model. + * + * @param context the specified request context + */ + @RequestProcessing(value = "/plugins/kanbanniang/assets/model", method = HttpMethod.GET) + public void randomModel(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + try { + final String assets = "/plugins/kanbanniang/assets"; + String model; + final ServletContext servletContext = SoloServletListener.getServletContext(); + try (final InputStream inputStream = servletContext.getResourceAsStream(assets + "/model-list.json")) { + final JSONArray models = new JSONArray(IOUtils.toString(inputStream, "UTF-8")); + final int i = RandomUtils.nextInt(models.length()); + model = models.getString(i); + } + + try (final InputStream modelResource = servletContext.getResourceAsStream(assets + "/model/" + model + "/index.json")) { + final JSONObject index = new JSONObject(IOUtils.toString(modelResource, "UTF-8")); + final JSONArray textures = index.optJSONArray("textures"); + if (textures.isEmpty()) { + try (final InputStream texturesRes = servletContext.getResourceAsStream(assets + "/model/" + model + "/textures.json")) { + final JSONArray texturesArray = new JSONArray(IOUtils.toString(texturesRes, "UTF-8")); + final Object element = texturesArray.opt(RandomUtils.nextInt(texturesArray.length())); + if (element instanceof JSONArray) { + index.put("textures", element); + } else { + index.put("textures", new JSONArray().put(element)); + } + } + } + renderer.setJSONObject(index); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Returns a random KanBanNiang model failed", e); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/OAuthProcessor.java b/src/main/java/org/b3log/solo/processor/OAuthProcessor.java new file mode 100644 index 00000000..850ab2c2 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/OAuthProcessor.java @@ -0,0 +1,244 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.util.Requests; +import org.b3log.latke.util.URLs; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.service.*; +import org.b3log.solo.util.GitHubs; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * OAuth processor. + *
    + *
  • Redirects to auth page (/oauth/github/redirect), GET
  • + *
  • OAuth callback (/oauth/github), GET
  • + *
+ * + * @author Liang Ding + * @version 1.0.1.1, Sep 12, 2019 + * @since 2.9.5 + */ +@RequestProcessor +public class OAuthProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(OAuthProcessor.class); + + /** + * OAuth parameters - state. + */ + private static final Set STATES = ConcurrentHashMap.newKeySet(); + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Option management service. + */ + @Inject + private OptionMgmtService optionMgmtService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Initialization service. + */ + @Inject + private InitService initService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Redirects to auth page. + * + * @param context the specified context + */ + @RequestProcessing(value = "/oauth/github/redirect", method = HttpMethod.GET) + public void redirectAuth(final RequestContext context) { + final HttpResponse res = HttpRequest.get("https://hacpai.com/oauth/solo/client2").trustAllCerts(true). + connectionTimeout(3000).timeout(7000).header("User-Agent", Solos.USER_AGENT).send(); + if (HttpServletResponse.SC_OK != res.statusCode()) { + LOGGER.log(Level.ERROR, "Gets oauth client id failed: " + res.toString()); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + res.charset("UTF-8"); + final JSONObject result = new JSONObject(res.bodyText()); + if (0 != result.optInt(Keys.CODE)) { + LOGGER.log(Level.ERROR, "Gets oauth client id failed: " + result.optString(Keys.MSG)); + + return; + } + final JSONObject data = result.optJSONObject(Keys.DATA); + final String clientId = data.optString("clientId"); + final String loginAuthURL = data.optString("loginAuthURL"); + + String referer = context.param("referer"); + if (StringUtils.isBlank(referer)) { + referer = Latkes.getServePath(); + } + final String cb = Latkes.getServePath() + "/oauth/github"; + String state = referer + ":::" + RandomStringUtils.randomAlphanumeric(16) + ":::cb=" + cb + ":::"; + STATES.add(state); + + final String path = loginAuthURL + "?client_id=" + clientId + "&state=" + URLs.encode(state) + "&scope=public_repo,read:user,user:follow"; + + context.sendRedirect(path); + } + + /** + * OAuth callback. + * + * @param context the specified context + */ + @RequestProcessing(value = "/oauth/github", method = HttpMethod.GET) + public synchronized void authCallback(final RequestContext context) { + String state = context.param("state"); + if (!STATES.contains(state)) { + context.sendError(HttpServletResponse.SC_BAD_REQUEST); + + return; + } + STATES.remove(state); + final String referer = URLs.decode(state); + final String accessToken = context.param("ak"); + final JSONObject userInfo = GitHubs.getGitHubUserInfo(accessToken); + if (null == userInfo) { + LOGGER.log(Level.WARN, "Can't get user info with token [" + accessToken + "]"); + context.sendError(HttpServletResponse.SC_UNAUTHORIZED); + + return; + } + + final HttpServletResponse response = context.getResponse(); + final HttpServletRequest request = context.getRequest(); + final String openId = userInfo.optString("openId"); + final String userName = userInfo.optString(User.USER_NAME); + final String userAvatar = userInfo.optString(UserExt.USER_AVATAR); + + JSONObject user = userQueryService.getUserByGitHubId(openId); + if (null == user) { + if (!initService.isInited()) { + final JSONObject initReq = new JSONObject(); + initReq.put(User.USER_NAME, userName); + initReq.put(UserExt.USER_AVATAR, userAvatar); + initReq.put(UserExt.USER_B3_KEY, openId); + initReq.put(UserExt.USER_GITHUB_ID, openId); + initService.init(initReq); + } else { + user = userQueryService.getUserByName(userName); + if (null == user) { + final JSONObject addUserReq = new JSONObject(); + addUserReq.put(User.USER_NAME, userName); + addUserReq.put(UserExt.USER_AVATAR, userAvatar); + addUserReq.put(User.USER_ROLE, Role.VISITOR_ROLE); + addUserReq.put(UserExt.USER_GITHUB_ID, openId); + addUserReq.put(UserExt.USER_B3_KEY, openId); + try { + userMgmtService.addUser(addUserReq); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Registers via oauth failed", e); + context.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + + return; + } + } else { + user.put(UserExt.USER_GITHUB_ID, openId); + try { + userMgmtService.updateUser(user); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Updates user GitHub id failed", e); + context.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + + return; + } + } + } + } else { + // 更改账号后无法登录 https://github.com/b3log/solo/issues/12879 + // 使用 GitHub 登录名覆盖本地用户名,解决 GitHub 改名后引起的登录问题 + user.put(User.USER_NAME, userName); + try { + userMgmtService.updateUser(user); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Updates user name failed", e); + context.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + + return; + } + } + + user = userQueryService.getUserByName(userName); + if (null == user) { + LOGGER.log(Level.WARN, "Can't get user by name [" + userName + "]"); + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final String redirect = StringUtils.substringBeforeLast(referer, "__"); + Solos.login(user, response); + context.sendRedirect(redirect); + LOGGER.log(Level.INFO, "Logged in [name={0}, remoteAddr={1}] with oauth", userName, Requests.getRemoteAddr(request)); + } +} diff --git a/src/main/java/org/b3log/solo/processor/PermalinkHandler.java b/src/main/java/org/b3log/solo/processor/PermalinkHandler.java new file mode 100644 index 00000000..5f5f3ab7 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/PermalinkHandler.java @@ -0,0 +1,137 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.servlet.DispatcherServlet; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.handler.Handler; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.service.InitService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.PermalinkQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; + +/** + * Article permalink handler. + * + * @author Liang Ding + * @version 1.0.0.2, May 18, 2019 + * @since 3.2.0 + */ +public class PermalinkHandler implements Handler { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PermalinkHandler.class); + + /** + * Whether initialization info reported. + */ + private static boolean initReported; + + @Override + public void handle(final RequestContext context) { + final BeanManager beanManager = BeanManager.getInstance(); + + JSONObject article; + try { + final InitService initService = beanManager.getReference(InitService.class); + if (!initService.isInited()) { + context.handle(); + + return; + } + + final String requestURI = context.requestURI(); + final String contextPath = Latkes.getContextPath(); + final String permalink = StringUtils.substringAfter(requestURI, contextPath); + if (PermalinkQueryService.invalidPermalinkFormat(permalink)) { + LOGGER.log(Level.DEBUG, "Skip permalink handling request [URI={0}]", permalink); + context.handle(); + + return; + } + + final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class); + article = articleRepository.getByPermalink(permalink); + if (null == article) { + LOGGER.log(Level.DEBUG, "Not found article with permalink [{0}]", permalink); + context.handle(); + + return; + } + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Processes article permalink handler failed", e); + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + // If requests an article and the article need view password, sends redirect to the password form + if (Solos.needViewPwd(context, article)) { + try { + context.sendRedirect(Latkes.getServePath() + "/console/article-pwd?articleId=" + article.optString(Keys.OBJECT_ID)); + + return; + } catch (final Exception e) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + } + + final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class); + final JSONObject preference = optionQueryService.getPreference(); + final boolean allowVisitDraftViaPermalink = preference.getBoolean(Option.ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK); + if (Article.ARTICLE_STATUS_C_PUBLISHED != article.optInt(Article.ARTICLE_STATUS) && !allowVisitDraftViaPermalink) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + dispatchToArticleProcessor(context, article); + context.handle(); + } + + /** + * Dispatches the specified request to the specified article processor with the specified response. + * + * @param context the specified request context + * @param article the specified article + * @see DispatcherServlet#result(RequestContext) + */ + private void dispatchToArticleProcessor(final RequestContext context, final JSONObject article) { + context.attr(Article.ARTICLE, article); + context.attr(Keys.HttpRequest.REQUEST_URI, Latkes.getContextPath() + "/article"); + context.attr(Keys.HttpRequest.REQUEST_METHOD, HttpMethod.GET.name()); + } +} diff --git a/src/main/java/org/b3log/solo/processor/SearchProcessor.java b/src/main/java/org/b3log/solo/processor/SearchProcessor.java new file mode 100644 index 00000000..3e0ac859 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/SearchProcessor.java @@ -0,0 +1,165 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.servlet.renderer.TextXmlRenderer; +import org.b3log.latke.util.Paginator; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.ArticleQueryService; +import org.b3log.solo.service.DataModelService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.UserQueryService; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; +import org.owasp.encoder.Encode; + +import javax.servlet.http.HttpServletRequest; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Search processor. + * + * @author Liang Ding + * @author Liyuan Li + * @version 1.1.1.3, Mar 19, 2019 + * @since 2.4.0 + */ +@RequestProcessor +public class SearchProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SearchProcessor.class); + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Shows opensearch.xml. + * + * @param context the specified context + */ + @RequestProcessing(value = "/opensearch.xml", method = HttpMethod.GET) + public void showOpensearchXML(final RequestContext context) { + final TextXmlRenderer renderer = new TextXmlRenderer(); + context.setRenderer(renderer); + + try { + final InputStream resourceAsStream = SearchProcessor.class.getResourceAsStream("/opensearch.xml"); + String content = IOUtils.toString(resourceAsStream, "UTF-8"); + final JSONObject preference = optionQueryService.getPreference(); + content = StringUtils.replace(content, "${blogTitle}", Jsoup.clean(preference.optString(Option.ID_C_BLOG_TITLE), Whitelist.none())); + content = StringUtils.replace(content, "${blogSubtitle}", Jsoup.clean(preference.optString(Option.ID_C_BLOG_SUBTITLE), Whitelist.none())); + content = StringUtils.replace(content, "${servePath}", Latkes.getServePath()); + + renderer.setContent(content); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Shows opensearch.xml failed", e); + } + } + + /** + * Searches articles. + * + * @param context the specified context + */ + @RequestProcessing(value = "/search", method = HttpMethod.GET) + public void search(final RequestContext context) { + final HttpServletRequest request = context.getRequest(); + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "common-template/search.ftl"); + final Map langs = langPropsService.getAll(Latkes.getLocale()); + final Map dataModel = renderer.getDataModel(); + dataModel.putAll(langs); + + final int pageNum = Paginator.getPage(request); + String keyword = context.param(Common.KEYWORD); + if (StringUtils.isBlank(keyword)) { + keyword = ""; + } + keyword = Encode.forHtml(keyword); + + dataModel.put(Common.KEYWORD, keyword); + final JSONObject result = articleQueryService.searchKeyword(keyword, pageNum, 15); + final List articles = (List) result.opt(Article.ARTICLES); + + try { + final JSONObject preference = optionQueryService.getPreference(); + + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + dataModelService.setArticlesExProperties(context, articles, preference); + + dataModel.put(Article.ARTICLES, articles); + final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION); + pagination.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum); + dataModel.put(Pagination.PAGINATION, pagination); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Search articles failed"); + + dataModel.put(Article.ARTICLES, Collections.emptyList()); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/SitemapProcessor.java b/src/main/java/org/b3log/solo/processor/SitemapProcessor.java new file mode 100644 index 00000000..87606bb9 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/SitemapProcessor.java @@ -0,0 +1,226 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.FilterOperator; +import org.b3log.latke.repository.PropertyFilter; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.SortDirection; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.TextXmlRenderer; +import org.b3log.latke.util.URLs; +import org.b3log.latke.util.XMLs; +import org.b3log.solo.model.ArchiveDate; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Page; +import org.b3log.solo.model.Tag; +import org.b3log.solo.model.sitemap.Sitemap; +import org.b3log.solo.model.sitemap.URL; +import org.b3log.solo.repository.ArchiveDateRepository; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.PageRepository; +import org.b3log.solo.repository.TagRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; + +/** + * Sitemap processor. + * + * @author Liang Ding + * @version 1.0.2.6, Jan 15, 2019 + * @since 0.3.1 + */ +@RequestProcessor +public class SitemapProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SitemapProcessor.class); + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Returns the sitemap. + * + * @param context the specified context + */ + @RequestProcessing(value = "/sitemap.xml", method = HttpMethod.GET) + public void sitemap(final RequestContext context) { + final TextXmlRenderer renderer = new TextXmlRenderer(); + context.setRenderer(renderer); + + try { + final Sitemap sitemap = new Sitemap(); + addArticles(sitemap); + addNavigations(sitemap); + addTags(sitemap); + addArchives(sitemap); + + String content = sitemap.toString(); + content = XMLs.format(content); + LOGGER.log(Level.INFO, "Generated sitemap"); + renderer.setContent(content); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Generates sitemap failed", e); + + context.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + } + } + + /** + * Adds articles into the specified sitemap. + * + * @param sitemap the specified sitemap + * @throws Exception exception + */ + private void addArticles(final Sitemap sitemap) throws Exception { + final Query query = new Query().setPage(1, Integer.MAX_VALUE). + setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)). + addSort(Article.ARTICLE_CREATED, SortDirection.DESCENDING). + select(Article.ARTICLE_PERMALINK, Article.ARTICLE_UPDATED); + final JSONObject articleResult = articleRepository.get(query); + final JSONArray articles = articleResult.getJSONArray(Keys.RESULTS); + + for (int i = 0; i < articles.length(); i++) { + final JSONObject article = articles.getJSONObject(i); + final String permalink = article.getString(Article.ARTICLE_PERMALINK); + + final URL url = new URL(); + url.setLoc(StringEscapeUtils.escapeXml(Latkes.getServePath() + permalink)); + final long updated = article.getLong(Article.ARTICLE_UPDATED); + final String lastMod = DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.format(updated); + url.setLastMod(lastMod); + + sitemap.addURL(url); + } + } + + /** + * Adds navigations into the specified sitemap. + * + * @param sitemap the specified sitemap + * @throws Exception exception + */ + private void addNavigations(final Sitemap sitemap) throws Exception { + final JSONObject result = pageRepository.get(new Query()); + final JSONArray pages = result.getJSONArray(Keys.RESULTS); + + for (int i = 0; i < pages.length(); i++) { + final JSONObject page = pages.getJSONObject(i); + final String permalink = page.getString(Page.PAGE_PERMALINK); + + final URL url = new URL(); + + // The navigation maybe a page or a link + // Just filters for user mistakes tolerance + if (!permalink.contains("://")) { + url.setLoc(Latkes.getServePath() + permalink); + } else { + url.setLoc(permalink); + } + + sitemap.addURL(url); + } + } + + /** + * Adds tags (tag-articles) and tags wall (/tags.html) into the specified sitemap. + * + * @param sitemap the specified sitemap + * @throws Exception exception + */ + private void addTags(final Sitemap sitemap) throws Exception { + final JSONObject result = tagRepository.get(new Query()); + final JSONArray tags = result.getJSONArray(Keys.RESULTS); + + for (int i = 0; i < tags.length(); i++) { + final JSONObject tag = tags.getJSONObject(i); + final String link = URLs.encode(tag.getString(Tag.TAG_TITLE)); + + final URL url = new URL(); + + url.setLoc(Latkes.getServePath() + "/tags/" + link); + + sitemap.addURL(url); + } + + // Tags wall + final URL url = new URL(); + + url.setLoc(Latkes.getServePath() + "/tags.html"); + sitemap.addURL(url); + } + + /** + * Adds archives (archive-articles) into the specified sitemap. + * + * @param sitemap the specified sitemap + * @throws Exception exception + */ + private void addArchives(final Sitemap sitemap) throws Exception { + final JSONObject result = archiveDateRepository.get(new Query()); + final JSONArray archiveDates = result.getJSONArray(Keys.RESULTS); + + for (int i = 0; i < archiveDates.length(); i++) { + final JSONObject archiveDate = archiveDates.getJSONObject(i); + final long time = archiveDate.getLong(ArchiveDate.ARCHIVE_TIME); + final String dateString = DateFormatUtils.format(time, "yyyy/MM"); + + final URL url = new URL(); + + url.setLoc(Latkes.getServePath() + "/archives/" + dateString); + + sitemap.addURL(url); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/SkinRenderer.java b/src/main/java/org/b3log/solo/processor/SkinRenderer.java new file mode 100644 index 00000000..ce042d84 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/SkinRenderer.java @@ -0,0 +1,129 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import freemarker.template.Template; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.solo.util.Skins; + +import javax.servlet.http.HttpServletRequest; +import java.io.StringWriter; +import java.util.Map; + +/** + * Skin renderer. + * + * @author Liang Ding + * @version 1.0.0.3, Jan 5, 2019 + * @since 2.9.1 + */ +public final class SkinRenderer extends AbstractFreeMarkerRenderer { + + /** + * HTTP servlet request context. + */ + private final RequestContext context; + + /** + * Constructs a skin renderer with the specified request context and template name. + * + * @param context the specified request context + * @param templateName the specified template name + */ + public SkinRenderer(final RequestContext context, final String templateName) { + this.context = context; + this.context.setRenderer(this); + setTemplateName(templateName); + } + + @Override + protected Template getTemplate() { + final String templateName = getTemplateName(); + Template ret = Skins.getSkinTemplate(context, templateName); + if (null == ret) { + // 优先使用皮肤内的登录、报错等模板 https://github.com/b3log/solo/issues/12566 + ret = Skins.getTemplate(templateName); + } + + return ret; + } + + /** + * Processes the specified FreeMarker template with the specified request, data model, pjax hacking. + * + * @param request the specified request + * @param dataModel the specified data model + * @param template the specified FreeMarker template + * @return generated HTML + * @throws Exception exception + */ + @Override + protected String genHTML(final HttpServletRequest request, final Map dataModel, final Template template) + throws Exception { + final boolean isPJAX = isPJAX(context); + dataModel.put("pjax", isPJAX); + + if (!isPJAX) { + return super.genHTML(request, dataModel, template); + } + + final StringWriter stringWriter = new StringWriter(); + template.setOutputEncoding("UTF-8"); + template.process(dataModel, stringWriter); + final long endTimeMillis = System.currentTimeMillis(); + final String dateString = DateFormatUtils.format(endTimeMillis, "yyyy/MM/dd HH:mm:ss"); + final long startTimeMillis = (Long) context.attr(Keys.HttpRequest.START_TIME_MILLIS); + final String latke = String.format("\n", endTimeMillis - startTimeMillis, dateString); + final String pjaxContainer = context.header("X-PJAX-Container"); + + final String html = stringWriter.toString(); + final String[] containers = StringUtils.substringsBetween(html, + "", + ""); + if (null == containers) { + return html + latke; + } + + return String.join("", containers) + latke; + } + + @Override + protected void beforeRender(final RequestContext context) { + } + + @Override + protected void afterRender(final RequestContext context) { + } + + /** + * Determines whether the specified request is sending with pjax. + * + * @param context the specified request context + * @return {@code true} if it is sending with pjax, otherwise returns {@code false} + */ + private static boolean isPJAX(final RequestContext context) { + final boolean pjax = Boolean.valueOf(context.header("X-PJAX")); + final String pjaxContainer = context.header("X-PJAX-Container"); + + return pjax && StringUtils.isNotBlank(pjaxContainer); + } +} diff --git a/src/main/java/org/b3log/solo/processor/TagProcessor.java b/src/main/java/org/b3log/solo/processor/TagProcessor.java new file mode 100644 index 00000000..0a069e04 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/TagProcessor.java @@ -0,0 +1,189 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.util.Paginator; +import org.b3log.latke.util.URLs; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.Tag; +import org.b3log.solo.service.*; +import org.b3log.solo.util.Skins; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * Tag processor. + * + * @author Liang Ding + * @version 1.1.1.8, Jan 5, 2019 + * @since 0.3.1 + */ +@RequestProcessor +public class TagProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(TagProcessor.class); + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Shows articles related with a tag with the specified context. + * + * @param context the specified context + */ + @RequestProcessing(value = "/tags/{tagTitle}", method = HttpMethod.GET) + public void showTagArticles(final RequestContext context) { + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "tag-articles.ftl"); + final Map dataModel = renderer.getDataModel(); + final HttpServletRequest request = context.getRequest(); + final HttpServletResponse response = context.getResponse(); + + try { + String tagTitle = context.pathVar("tagTitle"); + final int currentPageNum = Paginator.getPage(request); + LOGGER.log(Level.DEBUG, "Tag [title={0}, currentPageNum={1}]", tagTitle, currentPageNum); + final JSONObject result = tagQueryService.getTagByTitle(tagTitle); + if (null == result) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final JSONObject tag = result.getJSONObject(Tag.TAG); + final String tagId = tag.getString(Keys.OBJECT_ID); + + final JSONObject preference = optionQueryService.getPreference(); + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + final JSONObject tagArticleResult = articleQueryService.getArticlesByTag(tagId, currentPageNum, pageSize); + if (null == tagArticleResult) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + final List articles = (List) tagArticleResult.opt(Keys.RESULTS); + dataModelService.setArticlesExProperties(context, articles, preference); + + final int pageCount = tagArticleResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + fillPagination(dataModel, pageCount, currentPageNum, articles, pageNums); + dataModel.put(Common.PATH, "/tags/" + URLs.encode(tagTitle)); + dataModel.put(Keys.OBJECT_ID, tagId); + dataModel.put(Tag.TAG, tag); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + statisticMgmtService.incBlogViewCount(context, response); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Fills pagination. + * + * @param dataModel the specified data model + * @param pageCount the specified page count + * @param currentPageNum the specified current page number + * @param articles the specified articles + * @param pageNums the specified page numbers + */ + private void fillPagination(final Map dataModel, + final int pageCount, final int currentPageNum, + final List articles, + final List pageNums) { + final String previousPageNum = Integer.toString(currentPageNum > 1 ? currentPageNum - 1 : 0); + + dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, "0".equals(previousPageNum) ? "" : previousPageNum); + if (pageCount == currentPageNum + 1) { // The next page is the last page + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, ""); + } else { + dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, currentPageNum + 1); + } + dataModel.put(Article.ARTICLES, articles); + dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0)); + dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1)); + dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + } +} diff --git a/src/main/java/org/b3log/solo/processor/UserTemplateProcessor.java b/src/main/java/org/b3log/solo/processor/UserTemplateProcessor.java new file mode 100644 index 00000000..f583e753 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/UserTemplateProcessor.java @@ -0,0 +1,125 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import freemarker.template.Template; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.HttpMethod; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.RequestProcessing; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.util.Locales; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.DataModelService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.StatisticMgmtService; +import org.b3log.solo.util.Skins; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * User template processor. + * + *

+ * User can add a template (for example "links.ftl") then visits the page ("links.html"). + *

+ * + * @author Liang Ding + * @version 1.0.0.10, Jan 5, 2019 + * @since 0.4.5 + */ +@RequestProcessor +public class UserTemplateProcessor { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleProcessor.class); + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Shows the user template page. + * + * @param context the specified context + */ + @RequestProcessing(value = "/{name}.html", method = HttpMethod.GET) + public void showPage(final RequestContext context) { + final String requestURI = context.requestURI(); + final String templateName = context.pathVar("name") + ".ftl"; + LOGGER.log(Level.DEBUG, "Shows page [requestURI={0}, templateName={1}]", requestURI, templateName); + + final HttpServletRequest request = context.getRequest(); + final HttpServletResponse response = context.getResponse(); + final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, templateName); + + final Map dataModel = renderer.getDataModel(); + final Template template = Skins.getSkinTemplate(context, templateName); + if (null == template) { + context.sendError(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + try { + final Map langs = langPropsService.getAll(Locales.getLocale(request)); + dataModel.putAll(langs); + final JSONObject preference = optionQueryService.getPreference(); + dataModelService.fillCommon(context, dataModel, preference); + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + dataModelService.fillUserTemplate(context, template, dataModel, preference); + Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMAPLTE_DIR_NAME), dataModel); + statisticMgmtService.incBlogViewCount(context, response); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + context.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/AdminConsole.java b/src/main/java/org/b3log/solo/processor/console/AdminConsole.java new file mode 100644 index 00000000..752b6669 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/AdminConsole.java @@ -0,0 +1,482 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import jodd.io.ZipUtil; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.event.Event; +import org.b3log.latke.event.EventManager; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Plugin; +import org.b3log.latke.model.User; +import org.b3log.latke.plugin.ViewLoadEventData; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.latke.util.Execs; +import org.b3log.latke.util.Strings; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.service.DataModelService; +import org.b3log.solo.service.ExportService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.UserQueryService; +import org.b3log.solo.util.Markdowns; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.*; + +/** + * Admin console render processing. + * + * @author Liang Ding + * @version 1.7.0.16, Sep 17, 2019 + * @since 0.4.1 + */ +@Singleton +@Before(ConsoleAuthAdvice.class) +public class AdminConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(AdminConsole.class); + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Export service. + */ + @Inject + private ExportService exportService; + + /** + * DataModelService. + */ + @Inject + private DataModelService dataModelService; + + /** + * Event manager. + */ + @Inject + private EventManager eventManager; + + /** + * Shows administrator index with the specified context. + * + * @param context the specified context + */ + public void showAdminIndex(final RequestContext context) { + final String templateName = "admin-index.ftl"; + final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, templateName); + final Map langs = langPropsService.getAll(Latkes.getLocale()); + final Map dataModel = renderer.getDataModel(); + dataModel.putAll(langs); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + final String userName = currentUser.optString(User.USER_NAME); + dataModel.put(User.USER_NAME, userName); + final String roleName = currentUser.optString(User.USER_ROLE); + dataModel.put(User.USER_ROLE, roleName); + final String userAvatar = currentUser.optString(UserExt.USER_AVATAR); + dataModel.put(Common.GRAVATAR, userAvatar); + + try { + final JSONObject preference = optionQueryService.getPreference(); + dataModel.put(Option.ID_C_LOCALE_STRING, preference.getString(Option.ID_C_LOCALE_STRING)); + dataModel.put(Option.ID_C_BLOG_TITLE, preference.getString(Option.ID_C_BLOG_TITLE)); + dataModel.put(Option.ID_C_BLOG_SUBTITLE, preference.getString(Option.ID_C_BLOG_SUBTITLE)); + dataModel.put(Common.VERSION, SoloServletListener.VERSION); + dataModel.put(Common.STATIC_RESOURCE_VERSION, Latkes.getStaticResourceVersion()); + dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR))); + dataModel.put(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT, preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT)); + dataModel.put(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE, preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE)); + final JSONObject skin = optionQueryService.getSkin(); + dataModel.put(Option.CATEGORY_C_SKIN, skin.optString(Option.ID_C_SKIN_DIR_NAME)); + Keys.fillRuntime(dataModel); + dataModelService.fillMinified(dataModel); + dataModel.put(Common.LUTE_AVAILABLE, Markdowns.LUTE_AVAILABLE); + // 内置 HTTPS+CDN 文件存储 https://github.com/b3log/solo/issues/12556 + dataModel.put(Common.UPLOAD_TOKEN, ""); + dataModel.put(Common.UPLOAD_URL, ""); + dataModel.put(Common.UPLOAD_MSG, langPropsService.get("getUploadTokenErrLabel")); + final JSONObject upload = Solos.getUploadToken(context); + if (null != upload) { + dataModel.put(Common.UPLOAD_TOKEN, upload.optString(Common.UPLOAD_TOKEN)); + dataModel.put(Common.UPLOAD_URL, upload.optString(Common.UPLOAD_URL)); + dataModel.put(Common.UPLOAD_MSG, upload.optString(Common.UPLOAD_MSG)); + } + dataModelService.fillFaviconURL(dataModel, preference); + dataModelService.fillUsite(dataModel); + dataModelService.fillCommon(context, dataModel, preference); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Admin index render failed", e); + } + + fireFreeMarkerActionEvent(templateName, dataModel); + } + + /** + * Shows administrator functions with the specified context. + * + * @param context the specified context + */ + public void showAdminFunctions(final RequestContext context) { + final String requestURI = context.requestURI(); + final String templateName = StringUtils.substringBetween(requestURI, Latkes.getContextPath() + '/', ".") + ".ftl"; + final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, templateName); + + final Locale locale = Latkes.getLocale(); + final Map langs = langPropsService.getAll(locale); + final Map dataModel = renderer.getDataModel(); + + // 使用 MySQL 时不启用 SQL 导出功能 https://github.com/b3log/solo/issues/12806 + dataModel.put("supportExport", Latkes.RuntimeDatabase.H2 == Latkes.getRuntimeDatabase()); + dataModel.putAll(langs); + Keys.fillRuntime(dataModel); + dataModel.put(Option.ID_C_LOCALE_STRING, locale.toString()); + + fireFreeMarkerActionEvent(templateName, dataModel); + } + + /** + * Shows administrator preference function with the specified context. + * + * @param context the specified context + */ + public void showAdminPreferenceFunction(final RequestContext context) { + final String templateName = "admin-preference.ftl"; + final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, templateName); + + final Locale locale = Latkes.getLocale(); + final Map langs = langPropsService.getAll(locale); + final Map dataModel = renderer.getDataModel(); + dataModel.putAll(langs); + dataModel.put(Option.ID_C_LOCALE_STRING, locale.toString()); + + final JSONObject preference = optionQueryService.getPreference(); + final StringBuilder timeZoneIdOptions = new StringBuilder(); + final String[] availableIDs = TimeZone.getAvailableIDs(); + for (int i = 0; i < availableIDs.length; i++) { + final String id = availableIDs[i]; + String option; + + if (id.equals(preference.optString(Option.ID_C_TIME_ZONE_ID))) { + option = ""; + } else { + option = ""; + } + + timeZoneIdOptions.append(option); + } + + dataModel.put("timeZoneIdOptions", timeZoneIdOptions.toString()); + fireFreeMarkerActionEvent(templateName, dataModel); + } + + /** + * Exports data as SQL zip file. + * + * @param context the specified request context + */ + public void exportSQL(final RequestContext context) { + final HttpServletResponse response = context.getResponse(); + + if (!Solos.isAdminLoggedIn(context)) { + context.sendError(HttpServletResponse.SC_UNAUTHORIZED); + + return; + } + + try { + Thread.sleep(550); // 前端会发两次请求,文件名又是按秒生成,所以两次请求需要错开至少 1 秒避免文件名冲突 + } catch (final Exception e) { + // ignored + } + + final Latkes.RuntimeDatabase runtimeDatabase = Latkes.getRuntimeDatabase(); + if (Latkes.RuntimeDatabase.H2 != runtimeDatabase && Latkes.RuntimeDatabase.MYSQL != runtimeDatabase) { + context.renderJSON().renderMsg("Just support MySQL/H2 export now"); + + return; + } + + final String dbUser = Latkes.getLocalProperty("jdbc.username"); + final String dbPwd = Latkes.getLocalProperty("jdbc.password"); + final String dbURL = Latkes.getLocalProperty("jdbc.URL"); + String sql; // exported SQL script + + if (Latkes.RuntimeDatabase.MYSQL == runtimeDatabase) { + String db = StringUtils.substringAfterLast(dbURL, "/"); + db = StringUtils.substringBefore(db, "?"); + + try { + if (StringUtils.isNotBlank(dbPwd)) { + sql = Execs.exec("mysqldump -u" + dbUser + " -p" + dbPwd + " --databases " + db, 60 * 1000 * 5); + } else { + sql = Execs.exec("mysqldump -u" + dbUser + " --databases " + db, 60 * 1000 * 5); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Export failed", e); + context.renderJSON().renderMsg("Export failed, please check log"); + + return; + } + } else if (Latkes.RuntimeDatabase.H2 == runtimeDatabase) { + try (final Connection connection = Connections.getConnection(); + final Statement statement = connection.createStatement()) { + final StringBuilder sqlBuilder = new StringBuilder(); + final ResultSet resultSet = statement.executeQuery("SCRIPT"); + while (resultSet.next()) { + final String stmt = resultSet.getString(1); + sqlBuilder.append(stmt).append(Strings.LINE_SEPARATOR); + } + resultSet.close(); + + sql = sqlBuilder.toString(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Export failed", e); + context.renderJSON().renderMsg("Export failed, please check log"); + + return; + } + } else { + context.renderJSON().renderMsg("Just support MySQL/H2 export now"); + + return; + } + + if (StringUtils.isBlank(sql)) { + LOGGER.log(Level.ERROR, "Export failed, executing export script returns empty"); + context.renderJSON().renderMsg("Export failed, please check log"); + + return; + } + + final String tmpDir = System.getProperty("java.io.tmpdir"); + final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"); + String localFilePath = tmpDir + File.separator + "solo-" + date + ".sql"; + LOGGER.trace(localFilePath); + final File localFile = new File(localFilePath); + + try { + final byte[] data = sql.getBytes("UTF-8"); + try (final OutputStream output = new FileOutputStream(localFile)) { + IOUtils.write(data, output); + } + + final File zipFile = ZipUtil.zip(localFile); + byte[] zipData; + try (final FileInputStream inputStream = new FileInputStream(zipFile)) { + zipData = IOUtils.toByteArray(inputStream); + } + + response.setContentType("application/zip"); + final String fileName = "solo-sql-" + date + ".zip"; + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + + final ServletOutputStream outputStream = response.getOutputStream(); + outputStream.write(zipData); + outputStream.flush(); + outputStream.close(); + + // 导出 SQL 包后清理临时文件 https://github.com/b3log/solo/issues/12770 + localFile.delete(); + zipFile.delete(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Export failed", e); + context.renderJSON().renderMsg("Export failed, please check log"); + + return; + } + } + + /** + * Exports data as JSON zip file. + * + * @param context the specified request context + */ + public void exportJSON(final RequestContext context) { + final HttpServletResponse response = context.getResponse(); + if (!Solos.isAdminLoggedIn(context)) { + context.sendError(HttpServletResponse.SC_UNAUTHORIZED); + + return; + } + + try { + Thread.sleep(550); + } catch (final Exception e) { + // ignored + } + + final String tmpDir = System.getProperty("java.io.tmpdir"); + final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"); + String localFilePath = tmpDir + File.separator + "solo-" + date + ".json"; + LOGGER.trace(localFilePath); + final File localFile = new File(localFilePath); + + try { + final JSONObject json = exportService.getJSONs(); + final byte[] data = json.toString(4).getBytes("UTF-8"); + + try (final OutputStream output = new FileOutputStream(localFile)) { + IOUtils.write(data, output); + } + + try (final FileInputStream inputStream = new FileInputStream(ZipUtil.zip(localFile)); + final ServletOutputStream outputStream = response.getOutputStream()) { + final byte[] zipData = IOUtils.toByteArray(inputStream); + response.setContentType("application/zip"); + final String fileName = "solo-json-" + date + ".zip"; + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + outputStream.write(zipData); + outputStream.flush(); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Export failed", e); + context.renderJSON().renderMsg("Export failed, please check log"); + + return; + } + } + + /** + * Exports data as Hexo markdown zip file. + * + * @param context the specified request context + */ + public void exportHexo(final RequestContext context) { + final HttpServletResponse response = context.getResponse(); + if (!Solos.isAdminLoggedIn(context)) { + context.sendError(HttpServletResponse.SC_UNAUTHORIZED); + + return; + } + + try { + Thread.sleep(550); + } catch (final Exception e) { + // ignored + } + + final String tmpDir = System.getProperty("java.io.tmpdir"); + final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"); + String localFilePath = tmpDir + File.separator + "solo-hexo-" + date; + LOGGER.trace(localFilePath); + final File localFile = new File(localFilePath); + + final File postDir = new File(localFilePath + File.separator + "posts"); + final File passwordDir = new File(localFilePath + File.separator + "passwords"); + final File draftDir = new File(localFilePath + File.separator + "drafts"); + + try { + if (!postDir.mkdirs()) { + throw new Exception("Create dir [" + postDir.getPath() + "] failed"); + } + if (!passwordDir.mkdirs()) { + throw new Exception("Create dir [" + passwordDir.getPath() + "] failed"); + } + if (!draftDir.mkdirs()) { + throw new Exception("Create dir [" + draftDir.getPath() + "] failed"); + } + + final JSONObject result = exportService.exportHexoMDs(); + final List posts = (List) result.opt("posts"); + exportService.exportHexoMd(posts, postDir.getPath()); + final List passwords = (List) result.opt("passwords"); + exportService.exportHexoMd(passwords, passwordDir.getPath()); + final List drafts = (List) result.opt("drafts"); + exportService.exportHexoMd(drafts, draftDir.getPath()); + + final File zipFile = ZipUtil.zip(localFile); + byte[] zipData; + try (final FileInputStream inputStream = new FileInputStream(zipFile)) { + zipData = IOUtils.toByteArray(inputStream); + response.setContentType("application/zip"); + final String fileName = "solo-hexo-" + date + ".zip"; + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + } + + try (final ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(zipData); + outputStream.flush(); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Export failed", e); + context.renderJSON().renderMsg("Export failed, please check log"); + + return; + } + } + + /** + * Fires FreeMarker action event with the host template name and data model. + * + * @param hostTemplateName the specified host template name + * @param dataModel the specified data model + */ + private void fireFreeMarkerActionEvent(final String hostTemplateName, final Map dataModel) { + final ViewLoadEventData data = new ViewLoadEventData(); + + data.setViewName(hostTemplateName); + data.setDataModel(dataModel); + eventManager.fireEventSynchronously(new Event<>(Keys.FREEMARKER_ACTION, data)); + if (StringUtils.isBlank((String) dataModel.get(Plugin.PLUGINS))) { + // There is no plugin for this template, fill ${plugins} with blank. + dataModel.put(Plugin.PLUGINS, ""); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/ArticleConsole.java b/src/main/java/org/b3log/solo/processor/console/ArticleConsole.java new file mode 100644 index 00000000..9d8b1b46 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/ArticleConsole.java @@ -0,0 +1,566 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.latke.util.Strings; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.service.ArticleMgmtService; +import org.b3log.solo.service.ArticleQueryService; +import org.b3log.solo.service.UserQueryService; +import org.b3log.solo.util.Images; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Article console request processing. + * + * @author Liang Ding + * @version 1.2.0.2, May 18, 2019 + * @since 0.4.0 + */ +@Singleton +@Before(ConsoleAuthAdvice.class) +public class ArticleConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleConsole.class); + + /** + * Article management service. + */ + @Inject + private ArticleMgmtService articleMgmtService; + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Pushes an article to community. + * + * @param context the specified request context + */ + public void pushArticleToCommunity(final RequestContext context) { + final JSONObject result = new JSONObject().put(Keys.CODE, 0); + context.renderJSON(result); + final String articleId = context.param("id"); + articleMgmtService.pushArticleToCommunity(articleId); + } + + /** + * Gets article thumbs. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": true,
+     *     "data": [
+     *         "https://img.hacpai.com/bing/20171226.jpg?imageView2/1/w/960/h/540/interlace/1/q/100",
+     *         "https://img.hacpai.com/bing/20171105.jpg?imageView2/1/w/960/h/540/interlace/1/q/100",
+     *         "https://img.hacpai.com/bing/20180105.jpg?imageView2/1/w/960/h/540/interlace/1/q/100",
+     *         "https://img.hacpai.com/bing/20171114.jpg?imageView2/1/w/960/h/540/interlace/1/q/100"
+     *     ]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getArticleThumbs(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject result = new JSONObject(); + renderer.setJSONObject(result); + result.put(Keys.STATUS_CODE, true); + final HttpServletRequest request = context.getRequest(); + String strN = context.param("n"); + if (!Strings.isNumeric(strN)) { + strN = "6"; + } + + final int n = Integer.valueOf(strN); + final List urls = Images.randomImages(n); + + // original: 1920*1080 + + final String wStr = context.param("w"); + int w = Article.ARTICLE_THUMB_IMG_WIDTH; + if (Strings.isNumeric(wStr)) { + w = Integer.valueOf(wStr); + } + final int width = w; + final String hStr = context.param("h"); + int h = Article.ARTICLE_THUMB_IMG_HEIGHT; + if (Strings.isNumeric(hStr)) { + h = Integer.valueOf(hStr); + } + final int height = h; + + result.put("data", urls.stream().map(url -> Images.imageSize(url, width, height)).collect(Collectors.toList())); + } + + /** + * Gets an article by the specified request json object. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "oId": "",
+     *     "articleTitle": "",
+     *     "articleAbstract": "",
+     *     "articleContent": "",
+     *     "articlePermalink": "",
+     *     "articleTags": [{
+     *         "oId": "",
+     *         "tagTitle": ""
+     *     }, ....],
+     *     "articleSignId": "",
+     *     "signs": [{
+     *         "oId": "",
+     *         "signHTML": ""
+     *     }, ....]
+     *     "sc": "GET_ARTICLE_SUCC"
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + try { + final String articleId = context.pathVar("id"); + final JSONObject result = articleQueryService.getArticle(articleId); + result.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(result); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Gets articles(by crate date descending) by the specified request json object. + *

+ * The request URI contains the pagination arguments. For example, the request URI is + * /console/articles/status/published/1/10/20, means the current page is 1, the page size is 10, and the window size + * is 20. + *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "articles": [{
+     *         "oId": "",
+     *         "articleTitle": "",
+     *         "articleCommentCount": int,
+     *         "articleCreateTime"; long,
+     *         "articleViewCount": int,
+     *         "articleTags": "tag1, tag2, ....",
+     *         "articlePutTop": boolean,
+     *         "articleStatus": int
+     *      }, ....]
+     * }
+     * 
, order by article update date and sticky(put top). + *

+ * + * @param context the specified request context + */ + public void getArticles(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + try { + String path = context.requestURI().substring((Latkes.getContextPath() + "/console/articles/status/").length()); + final String status = StringUtils.substringBefore(path, "/"); + + path = path.substring((status + "/").length()); + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + requestJSONObject.put(Article.ARTICLE_STATUS, "published".equals(status) ? Article.ARTICLE_STATUS_C_PUBLISHED : Article.ARTICLE_STATUS_C_DRAFT); + + final JSONArray excludes = new JSONArray(); + excludes.put(Article.ARTICLE_CONTENT); + excludes.put(Article.ARTICLE_UPDATED); + excludes.put(Article.ARTICLE_CREATED); + excludes.put(Article.ARTICLE_AUTHOR_ID); + excludes.put(Article.ARTICLE_RANDOM_DOUBLE); + requestJSONObject.put(Keys.EXCLUDES, excludes); + + final String keyword = StringUtils.trim(context.param("k")); + if (StringUtils.isNotBlank(keyword)) { + requestJSONObject.put(Common.KEYWORD, keyword); + } + + final JSONObject result = articleQueryService.getArticles(requestJSONObject); + result.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(result); + + final JSONArray articles = result.optJSONArray(Article.ARTICLES); + for (int i = 0; i < articles.length(); i++) { + final JSONObject article = articles.optJSONObject(i); + String title = article.optString(Article.ARTICLE_TITLE); + title = StringEscapeUtils.escapeXml(title); + article.put(Article.ARTICLE_TITLE, title); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Removes an article by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void removeArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + final String articleId = context.pathVar("id"); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + + try { + if (!articleQueryService.canAccessArticle(articleId, currentUser)) { + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langPropsService.get("forbiddenLabel")); + + return; + } + + articleMgmtService.removeArticle(articleId); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } + + /** + * Cancels publish an article by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void cancelPublishArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + try { + final String articleId = context.pathVar("id"); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + if (!articleQueryService.canAccessArticle(articleId, currentUser)) { + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langPropsService.get("forbiddenLabel")); + + return; + } + + articleMgmtService.cancelPublishArticle(articleId); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("unPulbishSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("unPulbishFailLabel")); + } + } + + /** + * Cancels an top article by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void cancelTopArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + if (!Solos.isAdminLoggedIn(context)) { + ret.put(Keys.MSG, langPropsService.get("forbiddenLabel")); + ret.put(Keys.STATUS_CODE, false); + + return; + } + + try { + final String articleId = context.pathVar("id"); + articleMgmtService.topArticle(articleId, false); + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("cancelTopSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("cancelTopFailLabel")); + } + } + + /** + * Puts an article to top by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void putTopArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + if (!Solos.isAdminLoggedIn(context)) { + ret.put(Keys.MSG, langPropsService.get("forbiddenLabel")); + ret.put(Keys.STATUS_CODE, false); + + return; + } + + try { + final String articleId = context.pathVar("id"); + articleMgmtService.topArticle(articleId, true); + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("putTopSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("putTopFailLabel")); + } + } + + /** + * Updates an article by the specified request json object. + *

+ * The specified request json object, for example, + *

+     * {
+     *     "article": {
+     *         "oId": "",
+     *         "articleTitle": "",
+     *         "articleAbstract": "",
+     *         "articleContent": "",
+     *         "articleTags": "tag1,tag2,tag3", // optional, default set "待分类"
+     *         "articlePermalink": "", // optional
+     *         "articleStatus": int, // 0: published, 1: draft
+     *         "articleSignId": "" // optional
+     *         "articleCommentable": boolean,
+     *         "articleViewPwd": "",
+     *         "postToCommunity": boolean
+     *     }
+     *  }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void updateArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + try { + final JSONObject requestJSONObject = context.requestJSON(); + final JSONObject article = requestJSONObject.getJSONObject(Article.ARTICLE); + final String articleId = article.getString(Keys.OBJECT_ID); + renderer.setJSONObject(ret); + + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + if (!articleQueryService.canAccessArticle(articleId, currentUser)) { + ret.put(Keys.MSG, langPropsService.get("forbiddenLabel")); + ret.put(Keys.STATUS_CODE, false); + + return; + } + + // 打印请求日志,如果发生特殊情况丢失数据,至少还可以根据日志寻回内容 + LOGGER.log(Level.INFO, "Updates an article request [" + requestJSONObject.toString() + "]"); + + articleMgmtService.updateArticle(requestJSONObject); + + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + ret.put(Keys.STATUS_CODE, true); + } catch (final ServiceException e) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, e.getMessage()); + } + } + + /** + * Adds an article with the specified request. + *

+ * The specified request json object, for example, + *

+     * {
+     *     "article": {
+     *         "articleTitle": "",
+     *         "articleAbstract": "",
+     *         "articleContent": "",
+     *         "articleTags": "tag1,tag2,tag3", // optional, default set "待分类"
+     *         "articlePermalink": "", // optional
+     *         "articleStatus": int, // 0: published, 1: draft
+     *         "postToCommunity": boolean,
+     *         "articleSignId": "" // optional
+     *         "articleCommentable": boolean,
+     *         "articleViewPwd": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "oId": "", // Generated article id
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void addArticle(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + try { + final JSONObject requestJSONObject = context.requestJSON(); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + requestJSONObject.getJSONObject(Article.ARTICLE).put(Article.ARTICLE_AUTHOR_ID, currentUser.getString(Keys.OBJECT_ID)); + + // 打印请求日志,如果发生特殊情况丢失数据,至少还可以根据日志寻回内容 + LOGGER.log(Level.INFO, "Adds an article request [" + requestJSONObject.toString() + "]"); + + final String articleId = articleMgmtService.addArticle(requestJSONObject); + ret.put(Keys.OBJECT_ID, articleId); + ret.put(Keys.MSG, langPropsService.get("addSuccLabel")); + ret.put(Keys.STATUS_CODE, true); + + renderer.setJSONObject(ret); + } catch (final ServiceException e) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, e.getMessage()); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/CategoryConsole.java b/src/main/java/org/b3log/solo/processor/console/CategoryConsole.java new file mode 100644 index 00000000..fd82e8ea --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/CategoryConsole.java @@ -0,0 +1,543 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.latke.util.URLs; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Tag; +import org.b3log.solo.service.CategoryMgmtService; +import org.b3log.solo.service.CategoryQueryService; +import org.b3log.solo.service.TagQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Category console request processing. + * + * @author Liang Ding + * @author lzh984294471 + * @version 1.1.3.6, Sep 1, 2019 + * @since 2.0.0 + */ +@RequestProcessor +@Before(ConsoleAdminAuthAdvice.class) +public class CategoryConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CategoryConsole.class); + + /** + * Category management service. + */ + @Inject + private CategoryMgmtService categoryMgmtService; + + /** + * Category query service. + */ + @Inject + private CategoryQueryService categoryQueryService; + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Changes a category order by the specified category id and direction. + *

+ * Request json: + *

+     * {
+     *     "oId": "",
+     *     "direction": "" // "up"/"down"
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + * @throws Exception exception + */ + public void changeOrder(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + try { + final JSONObject requestJSON = context.requestJSON(); + final String categoryId = requestJSON.getString(Keys.OBJECT_ID); + final String direction = requestJSON.getString(Common.DIRECTION); + + categoryMgmtService.changeOrder(categoryId, direction); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + renderer.setJSONObject(ret); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Gets a category by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "category": {
+     *         "oId": "",
+     *         "categoryTitle": "",
+     *         "categoryURI": "",
+     *         ....
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + * @throws Exception exception + */ + @SuppressWarnings("unchecked") + public void getCategory(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + try { + final String categoryId = context.pathVar("id"); + final JSONObject result = categoryQueryService.getCategory(categoryId); + if (null == result) { + renderer.setJSONObject(new JSONObject().put(Keys.STATUS_CODE, false)); + + return; + } + + final StringBuilder tagBuilder = new StringBuilder(); + final List tags = (List) result.opt(Category.CATEGORY_T_TAGS); + for (final JSONObject tag : tags) { + if (null == tag || !tag.has(Tag.TAG_TITLE)) { // 修复修改分类时空指针错误 https://github.com/b3log/solo/pull/12876 + continue; + } + tagBuilder.append(tag.optString(Tag.TAG_TITLE)).append(","); + } + tagBuilder.deleteCharAt(tagBuilder.length() - 1); + result.put(Category.CATEGORY_T_TAGS, tagBuilder.toString()); + + renderer.setJSONObject(result); + result.put(Keys.STATUS_CODE, true); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Removes a category by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + * @throws Exception exception + */ + public void removeCategory(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + try { + final String categoryId = context.pathVar("id"); + categoryMgmtService.removeCategory(categoryId); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } + + /** + * Updates a category by the specified request. + *

+ * Request json: + *

+     * {
+     *     "oId": "",
+     *     "categoryTitle": "",
+     *     "categoryURI": "", // optional
+     *     "categoryDescription": "", // optional
+     *     "categoryTags": "tag1, tag2" // optional
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void updateCategory(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + try { + final JSONObject requestJSON = context.requestJSON(); + String tagsStr = requestJSON.optString(Category.CATEGORY_T_TAGS); + tagsStr = Tag.formatTags(tagsStr, 64); + if (StringUtils.isBlank(tagsStr)) { + throw new ServiceException(langPropsService.get("tagsEmptyLabel")); + } + final String[] tagTitles = tagsStr.split(","); + String addArticleWithTagFirstLabel = langPropsService.get("addArticleWithTagFirstLabel"); + + final List tags = new ArrayList<>(); + final Set deduplicate = new HashSet<>(); + for (int i = 0; i < tagTitles.length; i++) { + String tagTitle = StringUtils.trim(tagTitles[i]); + if (StringUtils.isBlank(tagTitle)) { + continue; + } + + final JSONObject tagResult = tagQueryService.getTagByTitle(tagTitle); + if (null == tagResult) { + addArticleWithTagFirstLabel = addArticleWithTagFirstLabel.replace("{tag}", tagTitle); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, addArticleWithTagFirstLabel); + + return; + } + + if (deduplicate.contains(tagTitle)) { + continue; + } + + tags.add(tagResult.optJSONObject(Tag.TAG)); + deduplicate.add(tagTitle); + } + + final String categoryId = requestJSON.optString(Keys.OBJECT_ID); + final String title = requestJSON.optString(Category.CATEGORY_TITLE, "Category"); + JSONObject mayExist = categoryQueryService.getByTitle(title); + if (null != mayExist && !mayExist.optString(Keys.OBJECT_ID).equals(categoryId)) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("duplicatedCategoryLabel")); + + return; + } + + String uri = requestJSON.optString(Category.CATEGORY_URI, title); + if (StringUtils.isBlank(uri)) { + uri = title; + } + uri = URLs.encode(uri); + mayExist = categoryQueryService.getByURI(uri); + if (null != mayExist && !mayExist.optString(Keys.OBJECT_ID).equals(categoryId)) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("duplicatedCategoryURILabel")); + + return; + } + if (255 <= StringUtils.length(uri)) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("categoryURITooLongLabel")); + + return; + } + + final String desc = requestJSON.optString(Category.CATEGORY_DESCRIPTION); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, title); + category.put(Category.CATEGORY_URI, uri); + category.put(Category.CATEGORY_DESCRIPTION, desc); + + categoryMgmtService.updateCategory(categoryId, category); + categoryMgmtService.removeCategoryTags(categoryId); // remove old relations + + // add new relations + for (final JSONObject tag : tags) { + final JSONObject categoryTag = new JSONObject(); + categoryTag.put(Category.CATEGORY + "_" + Keys.OBJECT_ID, categoryId); + categoryTag.put(Tag.TAG + "_" + Keys.OBJECT_ID, tag.optString(Keys.OBJECT_ID)); + + categoryMgmtService.addCategoryTag(categoryTag); + } + + ret.put(Keys.OBJECT_ID, categoryId); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + ret.put(Keys.STATUS_CODE, true); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Adds a category with the specified request. + *

+ * Request json: + *

+     * {
+     *     "categoryTitle": "",
+     *     "categoryURI": "", // optional
+     *     "categoryDescription": "", // optional
+     *     "categoryTags": "tag1, tag2" // optional
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "oId": "", // Generated category id
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void addCategory(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + try { + final JSONObject requestJSONObject = context.requestJSON(); + String tagsStr = requestJSONObject.optString(Category.CATEGORY_T_TAGS); + tagsStr = Tag.formatTags(tagsStr, 64); + if (StringUtils.isBlank(tagsStr)) { + throw new ServiceException(langPropsService.get("tagsEmptyLabel")); + } + final String[] tagTitles = tagsStr.split(","); + String addArticleWithTagFirstLabel = langPropsService.get("addArticleWithTagFirstLabel"); + + final List tags = new ArrayList<>(); + final Set deduplicate = new HashSet<>(); + for (int i = 0; i < tagTitles.length; i++) { + String tagTitle = StringUtils.trim(tagTitles[i]); + if (StringUtils.isBlank(tagTitle)) { + continue; + } + + final JSONObject tagResult = tagQueryService.getTagByTitle(tagTitle); + if (null == tagResult) { + addArticleWithTagFirstLabel = addArticleWithTagFirstLabel.replace("{tag}", tagTitle); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, addArticleWithTagFirstLabel); + + return; + } + + if (deduplicate.contains(tagTitle)) { + continue; + } + + tags.add(tagResult.optJSONObject(Tag.TAG)); + deduplicate.add(tagTitle); + } + + final String title = requestJSONObject.optString(Category.CATEGORY_TITLE, "Category"); + JSONObject mayExist = categoryQueryService.getByTitle(title); + if (null != mayExist) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("duplicatedCategoryLabel")); + + return; + } + + String uri = requestJSONObject.optString(Category.CATEGORY_URI, title); + if (StringUtils.isBlank(uri)) { + uri = title; + } + uri = URLs.encode(uri); + mayExist = categoryQueryService.getByURI(uri); + if (null != mayExist) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("duplicatedCategoryURILabel")); + + return; + } + if (255 <= StringUtils.length(uri)) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("categoryURITooLongLabel")); + + return; + } + + final String desc = requestJSONObject.optString(Category.CATEGORY_DESCRIPTION); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, title); + category.put(Category.CATEGORY_URI, uri); + category.put(Category.CATEGORY_DESCRIPTION, desc); + + final String categoryId = categoryMgmtService.addCategory(category); + + for (final JSONObject tag : tags) { + final JSONObject categoryTag = new JSONObject(); + categoryTag.put(Category.CATEGORY + "_" + Keys.OBJECT_ID, categoryId); + categoryTag.put(Tag.TAG + "_" + Keys.OBJECT_ID, tag.optString(Keys.OBJECT_ID)); + + categoryMgmtService.addCategoryTag(categoryTag); + } + + ret.put(Keys.OBJECT_ID, categoryId); + ret.put(Keys.MSG, langPropsService.get("addSuccLabel")); + ret.put(Keys.STATUS_CODE, true); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Gets categories by the specified request json object. + *

+ * The request URI contains the pagination arguments. For example, the request URI is /console/categories/1/10/20, means + * the current page is 1, the page size is 10, and the window size is 20. + *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "categories": [{
+     *         "oId": "",
+     *         "categoryTitle": "",
+     *         "categoryURI": "",
+     *         ....
+     *      }, ....]
+     *     "sc": true
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getCategories(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final HttpServletRequest request = context.getRequest(); + final String requestURI = context.requestURI(); + final String path = requestURI.substring((Latkes.getContextPath() + "/console/categories/").length()); + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + final JSONObject result = categoryQueryService.getCategoris(requestJSONObject); + result.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(result); + + final JSONArray categories = result.optJSONArray(Category.CATEGORIES); + for (int i = 0; i < categories.length(); i++) { + final JSONObject category = categories.optJSONObject(i); + String title = category.optString(Category.CATEGORY_TITLE); + title = StringEscapeUtils.escapeXml(title); + category.put(Category.CATEGORY_TITLE, title); + } + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/CommentConsole.java b/src/main/java/org/b3log/solo/processor/console/CommentConsole.java new file mode 100644 index 00000000..f1912c97 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/CommentConsole.java @@ -0,0 +1,212 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Comment; +import org.b3log.solo.service.CommentMgmtService; +import org.b3log.solo.service.CommentQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.List; + +/** + * Comment console request processing. + * + * @author Liang Ding + * @version 1.1.0.0, Apr 18, 2019 + * @since 0.4.0 + */ +@RequestProcessor +@Before(ConsoleAuthAdvice.class) +public class CommentConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CommentConsole.class); + + /** + * Comment query service. + */ + @Inject + private CommentQueryService commentQueryService; + + /** + * Comment management service. + */ + @Inject + private CommentMgmtService commentMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Removes a comment of an article by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void removeArticleComment(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + try { + final String commentId = context.pathVar("id"); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + if (!commentQueryService.canAccessComment(commentId, currentUser)) { + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langPropsService.get("forbiddenLabel")); + + return; + } + + commentMgmtService.removeArticleComment(commentId); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } + + /** + * Gets comments by the specified request. + *

+ * The request URI contains the pagination arguments. For example, the + * request URI is /console/comments/1/10/20, means the current page is 1, the + * page size is 10, and the window size is 20. + *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "comments": [{
+     *         "oId": "",
+     *         "commentTitle": "",
+     *         "commentName": "",
+     *         "thumbnailUrl": "",
+     *         "commentURL": "",
+     *         "commentContent": "",
+     *         "commentTime": long,
+     *         "commentSharpURL": ""
+     *      }, ....]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getComments(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String requestURI = context.requestURI(); + final String path = requestURI.substring((Latkes.getContextPath() + "/console/comments/").length()); + + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + final JSONObject result = commentQueryService.getComments(requestJSONObject); + + result.put(Keys.STATUS_CODE, true); + + renderer.setJSONObject(result); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Gets comments of an article specified by the article id for administrator. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "comments": [{
+     *         "oId": "",
+     *         "commentName": "",
+     *         "thumbnailUrl": "",
+     *         "commentURL": "",
+     *         "commentContent": "",
+     *         "commentTime": long,
+     *         "commentSharpURL": "",
+     *         "isReply": boolean
+     *      }, ....]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getArticleComments(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + try { + final String articleId = context.pathVar("id"); + final List comments = commentQueryService.getComments(articleId); + + ret.put(Comment.COMMENTS, comments); + ret.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/ConsoleAdminAuthAdvice.java b/src/main/java/org/b3log/solo/processor/console/ConsoleAdminAuthAdvice.java new file mode 100644 index 00000000..48ec2fa5 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/ConsoleAdminAuthAdvice.java @@ -0,0 +1,50 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.advice.ProcessAdvice; +import org.b3log.latke.servlet.advice.RequestProcessAdviceException; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; + +/** + * The common auth check before advice for admin console. + * + * @author Liang Ding + * @version 1.0.1.3, Oct 5, 2018 + * @since 2.9.5 + */ +@Singleton +public class ConsoleAdminAuthAdvice extends ProcessAdvice { + + @Override + public void doAdvice(final RequestContext context) throws RequestProcessAdviceException { + if (!Solos.isAdminLoggedIn(context)) { + final JSONObject exception401 = new JSONObject(); + exception401.put(Keys.MSG, "Unauthorized to request [" + context.requestURI() + "], please signin using admin account"); + exception401.put(Keys.STATUS_CODE, HttpServletResponse.SC_UNAUTHORIZED); + + throw new RequestProcessAdviceException(exception401); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/ConsoleAuthAdvice.java b/src/main/java/org/b3log/solo/processor/console/ConsoleAuthAdvice.java new file mode 100644 index 00000000..6cb441f8 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/ConsoleAuthAdvice.java @@ -0,0 +1,62 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Singleton; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.advice.ProcessAdvice; +import org.b3log.latke.servlet.advice.RequestProcessAdviceException; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; + +/** + * The common auth check before advice for admin console. + * + * @author Liang Ding + * @version 1.0.1.3, Feb 7, 2019 + * @since 2.9.5 + */ +@Singleton +public class ConsoleAuthAdvice extends ProcessAdvice { + + @Override + public void doAdvice(final RequestContext context) throws RequestProcessAdviceException { + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + if (null == currentUser) { + final JSONObject exception401 = new JSONObject(); + exception401.put(Keys.MSG, "Unauthorized to request [" + context.requestURI() + "], please signin"); + exception401.put(Keys.STATUS_CODE, HttpServletResponse.SC_UNAUTHORIZED); + + throw new RequestProcessAdviceException(exception401); + } + + final String userRole = currentUser.optString(User.USER_ROLE); + if (Role.VISITOR_ROLE.equals(userRole)) { + final JSONObject exception403 = new JSONObject(); + exception403.put(Keys.MSG, "Forbidden to request [" + context.requestURI() + "]"); + exception403.put(Keys.STATUS_CODE, HttpServletResponse.SC_FORBIDDEN); + + throw new RequestProcessAdviceException(exception403); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/ConsoleRenderer.java b/src/main/java/org/b3log/solo/processor/console/ConsoleRenderer.java new file mode 100644 index 00000000..7585fbf9 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/ConsoleRenderer.java @@ -0,0 +1,63 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import freemarker.template.Template; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.renderer.AbstractFreeMarkerRenderer; +import org.b3log.solo.util.Skins; + +/** + * FreeMarker HTTP response renderer for administrator console. + * + * @author Liang Ding + * @version 1.0.1.6, Feb 7, 2019 + * @since 0.4.1 + */ +public final class ConsoleRenderer extends AbstractFreeMarkerRenderer { + + /** + * HTTP servlet request context. + */ + private final RequestContext context; + + /** + * Constructs a skin renderer with the specified request context and template name. + * + * @param context the specified request context + * @param templateName the specified template name + */ + public ConsoleRenderer(final RequestContext context, final String templateName) { + this.context = context; + this.context.setRenderer(this); + setTemplateName("admin/" + templateName); + } + + @Override + protected Template getTemplate() { + return Skins.getTemplate(getTemplateName()); + } + + @Override + protected void beforeRender(final RequestContext context) { + } + + @Override + protected void afterRender(final RequestContext context) { + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/LinkConsole.java b/src/main/java/org/b3log/solo/processor/console/LinkConsole.java new file mode 100644 index 00000000..60c3e90b --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/LinkConsole.java @@ -0,0 +1,348 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringEscapeUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Link; +import org.b3log.solo.service.LinkMgmtService; +import org.b3log.solo.service.LinkQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Link console request processing. + * + * @author Liang Ding + * @version 1.0.1.4, Dec 11, 2018 + * @since 0.4.0 + */ +@RequestProcessor +@Before(ConsoleAdminAuthAdvice.class) +public class LinkConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(LinkConsole.class); + + /** + * Link query service. + */ + @Inject + private LinkQueryService linkQueryService; + + /** + * Link management service. + */ + @Inject + private LinkMgmtService linkMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Removes a link by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void removeLink(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + try { + final String linkId = context.pathVar("id"); + linkMgmtService.removeLink(linkId); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } + + /** + * Updates a link by the specified request. + *

+ * Request json: + *

+     * {
+     *     "link": {
+     *         "oId": "",
+     *         "linkTitle": "",
+     *         "linkAddress": "",
+     *         "linkDescription": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void updateLink(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSON = context.requestJSON(); + linkMgmtService.updateLink(requestJSON); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + renderer.setJSONObject(ret); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Changes a link order by the specified link id and direction. + *

+ * Request json: + *

+     * {
+     *     "oId": "",
+     *     "direction": "" // "up"/"down"
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void changeOrder(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSON = context.requestJSON(); + final String linkId = requestJSON.getString(Keys.OBJECT_ID); + final String direction = requestJSON.getString(Common.DIRECTION); + + linkMgmtService.changeOrder(linkId, direction); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + renderer.setJSONObject(ret); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Adds a link with the specified request. + *

+ *

+     * {
+     *     "link": {
+     *         "linkTitle": "",
+     *         "linkAddress": "",
+     *         "linkDescription": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "oId": "", // Generated link id
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void addLink(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSON = context.requestJSON(); + final String linkId = linkMgmtService.addLink(requestJSON); + + ret.put(Keys.OBJECT_ID, linkId); + ret.put(Keys.MSG, langPropsService.get("addSuccLabel")); + ret.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(ret); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("addFailLabel")); + } + } + + /** + * Gets links by the specified request. + *

+ * The request URI contains the pagination arguments. For example, the + * request URI is /console/links/1/10/20, means the current page is 1, the + * page size is 10, and the window size is 20. + *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "links": [{
+     *         "oId": "",
+     *         "linkTitle": "",
+     *         "linkAddress": "",
+     *         "linkDescription": ""
+     *      }, ....]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getLinks(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String requestURI = context.requestURI(); + final String path = requestURI.substring((Latkes.getContextPath() + "/console/links/").length()); + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + final JSONObject result = linkQueryService.getLinks(requestJSONObject); + result.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(result); + + final JSONArray links = result.optJSONArray(Link.LINKS); + for (int i = 0; i < links.length(); i++) { + final JSONObject link = links.optJSONObject(i); + String title = link.optString(Link.LINK_TITLE); + title = StringEscapeUtils.escapeXml(title); + link.put(Link.LINK_TITLE, title); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Gets the file with the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "link": {
+     *         "oId": "",
+     *         "linkTitle": "",
+     *         "linkAddress": "",
+     *         "linkDescription": ""
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getLink(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String linkId = context.pathVar("id"); + final JSONObject result = linkQueryService.getLink(linkId); + if (null == result) { + renderer.setJSONObject(new JSONObject().put(Keys.STATUS_CODE, false)); + + return; + } + + renderer.setJSONObject(result); + result.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/OtherConsole.java b/src/main/java/org/b3log/solo/processor/console/OtherConsole.java new file mode 100644 index 00000000..ab4a9ea4 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/OtherConsole.java @@ -0,0 +1,129 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.service.ArchiveDateMgmtService; +import org.b3log.solo.service.TagMgmtService; +import org.json.JSONObject; + +/** + * Other console request processing. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 20, 2019 + * @since 3.4.0 + */ +@RequestProcessor +public class OtherConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(OtherConsole.class); + + /** + * Tag management service. + */ + @Inject + private TagMgmtService tagMgmtService; + + /** + * ArchiveDate maangement service. + */ + @Inject + private ArchiveDateMgmtService archiveDateMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Removes all unused archives. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void removeUnusedArchives(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + try { + archiveDateMgmtService.removeUnusedArchiveDates(); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Removes unused archives failed", e); + + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } + + /** + * Removes all unused tags. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void removeUnusedTags(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + try { + tagMgmtService.removeUnusedTags(); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Removes unused tags failed", e); + + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/PageConsole.java b/src/main/java/org/b3log/solo/processor/console/PageConsole.java new file mode 100644 index 00000000..33984f3b --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/PageConsole.java @@ -0,0 +1,361 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringEscapeUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Page; +import org.b3log.solo.service.PageMgmtService; +import org.b3log.solo.service.PageQueryService; +import org.b3log.solo.service.UserQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Plugin console request processing. + * + * @author Liang Ding + * @version 1.0.0.12, Apr 22, 2019 + * @since 0.4.0 + */ +@RequestProcessor +@Before(ConsoleAdminAuthAdvice.class) +public class PageConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PageConsole.class); + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Page query service. + */ + @Inject + private PageQueryService pageQueryService; + + /** + * Page management service. + */ + @Inject + private PageMgmtService pageMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Updates a page by the specified request. + *

+ * Request json: + *

+     * {
+     *     "page": {
+     *         "oId": "",
+     *         "pageTitle": "",
+     *         "pageOrder": int,
+     *         "pagePermalink": "",
+     *         "pageOpenTarget": "",
+     *         "pageIcon": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void updatePage(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSON = context.requestJSON(); + pageMgmtService.updatePage(requestJSON); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + renderer.setJSONObject(ret); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Removes a page by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void removePage(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + try { + final String pageId = context.pathVar("id"); + pageMgmtService.removePage(pageId); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + + } + } + + /** + * Adds a page with the specified request. + *

+ * Request json: + *

+     * {
+     *     "page": {
+     *         "pageTitle": "",
+     *         "pagePermalink": "" // optional
+     *         "pageOpenTarget": "",
+     *         "pageIcon": ""
+     *     }
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "oId": "", // Generated page id
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void addPage(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSON = context.requestJSON(); + final String pageId = pageMgmtService.addPage(requestJSON); + + ret.put(Keys.OBJECT_ID, pageId); + ret.put(Keys.MSG, langPropsService.get("addSuccLabel")); + ret.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(ret); + } catch (final ServiceException e) { // May be permalink check exception + LOGGER.log(Level.WARN, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Changes a page order by the specified page id and direction. + *

+ * Request json: + *

+     * {
+     *     "oId": "",
+     *     "direction": "" // "up"/"down"
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void changeOrder(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSONObject = context.requestJSON(); + final String linkId = requestJSONObject.getString(Keys.OBJECT_ID); + final String direction = requestJSONObject.getString(Common.DIRECTION); + + pageMgmtService.changeOrder(linkId, direction); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + + renderer.setJSONObject(ret); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Gets a page by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean
+     *     "page": {
+     *         "oId": "",
+     *         "pageTitle": "",
+     *         "pageOrder": int,
+     *         "pagePermalink": "",
+     *         "pageIcon": ""
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getPage(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String pageId = context.pathVar("id"); + final JSONObject result = pageQueryService.getPage(pageId); + if (null == result) { + renderer.setJSONObject(new JSONObject().put(Keys.STATUS_CODE, false)); + + return; + } + + renderer.setJSONObject(result); + result.put(Keys.STATUS_CODE, true); + result.put(Keys.MSG, langPropsService.get("getSuccLabel")); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Gets pages by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "pages": [{
+     *         "oId": "",
+     *         "pageTitle": "",
+     *         "pageOrder": int,
+     *         "pagePermalink": "",
+     *         .{@link PageMgmtService...}
+     *      }, ....]
+     *     "sc": "GET_PAGES_SUCC"
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getPages(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String requestURI = context.requestURI(); + final String path = requestURI.substring((Latkes.getContextPath() + "/console/pages/").length()); + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + final JSONObject result = pageQueryService.getPages(requestJSONObject); + final JSONArray pages = result.optJSONArray(Page.PAGES); + + for (int i = 0; i < pages.length(); i++) { + final JSONObject page = pages.getJSONObject(i); + String title = page.optString(Page.PAGE_TITLE); + title = StringEscapeUtils.escapeXml(title); + page.put(Page.PAGE_TITLE, title); + } + + result.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(result); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/PluginConsole.java b/src/main/java/org/b3log/solo/processor/console/PluginConsole.java new file mode 100644 index 00000000..891e1aba --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/PluginConsole.java @@ -0,0 +1,191 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Plugin; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.service.PluginMgmtService; +import org.b3log.solo.service.PluginQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import java.util.Map; + +/** + * Plugin console request processing. + * + * @author Liang Ding + * @author Love Yao + * @version 1.1.0.5, Dec 11, 2018 + * @since 0.4.0 + */ +@RequestProcessor +@Before(ConsoleAdminAuthAdvice.class) +public class PluginConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PluginConsole.class); + + /** + * Plugin query service. + */ + @Inject + private PluginQueryService pluginQueryService; + + /** + * Plugin management service. + */ + @Inject + private PluginMgmtService pluginMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Sets a plugin's status with the specified plugin id, status. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void setPluginStatus(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + final JSONObject requestJSONObject = context.requestJSON(); + final String pluginId = requestJSONObject.getString(Keys.OBJECT_ID); + final String status = requestJSONObject.getString(Plugin.PLUGIN_STATUS); + final JSONObject result = pluginMgmtService.setPluginStatus(pluginId, status); + + renderer.setJSONObject(result); + } + + /** + * Gets plugins by the specified request. + *

+ * The request URI contains the pagination arguments. For example, the + * request URI is /console/plugins/1/10/20, means the current page is 1, the + * page size is 10, and the window size is 20. + *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "plugins": [{
+     *         "name": "",
+     *         "version": "",
+     *         "author": "",
+     *         "status": "", // Enumeration name of {@link org.b3log.latke.plugin.PluginStatus}
+     *      }, ....]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + * @throws Exception exception + */ + public void getPlugins(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String requestURI = context.requestURI(); + final String path = requestURI.substring((Latkes.getContextPath() + "/console/plugins/").length()); + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + final JSONObject result = pluginQueryService.getPlugins(requestJSONObject); + + renderer.setJSONObject(result); + result.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * get the info of the specified pluginoId,just fot the plugin-setting. + * + * @param context the specified request context + */ + public void toSetting(final RequestContext context) { + final ConsoleRenderer renderer = new ConsoleRenderer(context, "admin-plugin-setting.ftl"); + final Map dataModel = renderer.getDataModel(); + + try { + final JSONObject requestJSONObject = context.requestJSON(); + final String pluginId = requestJSONObject.getString(Keys.OBJECT_ID); + final String setting = pluginQueryService.getPluginSetting(pluginId); + Keys.fillRuntime(dataModel); + dataModel.put(Plugin.PLUGIN_SETTING, setting); + dataModel.put(Keys.OBJECT_ID, pluginId); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + final JsonRenderer JsonRenderer = new JsonRenderer(); + JsonRenderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * update the setting of the plugin. + * + * @param context the specified request context + */ + public void updateSetting(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + final JSONObject requestJSONObject = context.requestJSON(); + final String pluginoId = requestJSONObject.optString(Keys.OBJECT_ID); + final String settings = requestJSONObject.optString(Plugin.PLUGIN_SETTING); + final JSONObject ret = pluginMgmtService.updatePluginSetting(pluginoId, settings); + + renderer.setJSONObject(ret); + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/PreferenceConsole.java b/src/main/java/org/b3log/solo/processor/console/PreferenceConsole.java new file mode 100644 index 00000000..5def703d --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/PreferenceConsole.java @@ -0,0 +1,388 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.Sign; +import org.b3log.solo.service.OptionMgmtService; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.PreferenceMgmtService; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Preference console request processing. + * + * @author Liang Ding + * @author hzchendou + * @version 1.2.0.25, Jun 13, 2019 + * @since 0.4.0 + */ +@RequestProcessor +@Before(ConsoleAdminAuthAdvice.class) +public class PreferenceConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PreferenceConsole.class); + + /** + * Preference management service. + */ + @Inject + private PreferenceMgmtService preferenceMgmtService; + + /** + * Option management service. + */ + @Inject + private OptionMgmtService optionMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Gets signs. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "signs": [{
+     *         "oId": "",
+     *         "signHTML": ""
+     *      }, ...]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getSigns(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final JSONObject preference = optionQueryService.getPreference(); + final JSONArray signs = new JSONArray(); + final JSONArray allSigns = // includes the empty sign(id=0) + new JSONArray(preference.getString(Option.ID_C_SIGNS)); + + for (int i = 1; i < allSigns.length(); i++) { // excludes the empty sign + signs.put(allSigns.getJSONObject(i)); + } + + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + ret.put(Sign.SIGNS, signs); + ret.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Gets preference. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "preference": {
+     *         "mostViewArticleDisplayCount": int,
+     *         "recentCommentDisplayCount": int,
+     *         "mostUsedTagDisplayCount": int,
+     *         "articleListDisplayCount": int,
+     *         "articleListPaginationWindowSize": int,
+     *         "mostCommentArticleDisplayCount": int,
+     *         "externalRelevantArticlesDisplayCount": int,
+     *         "relevantArticlesDisplayCount": int,
+     *         "randomArticlesDisplayCount": int,
+     *         "blogTitle": "",
+     *         "blogSubtitle": "",
+     *         "localeString": "",
+     *         "timeZoneId": "",
+     *         "skinDirName": "",
+     *         "skins": "[{
+     *             "skinDirName": ""
+     *         }, ....]",
+     *         "noticeBoard": "",
+     *         "footerContent": "",
+     *         "htmlHead": "",
+     *         "metaKeywords": "",
+     *         "metaDescription": "",
+     *         "enableArticleUpdateHint": boolean,
+     *         "signs": "[{
+     *             "oId": "",
+     *             "signHTML": ""
+     *         }, ...]",
+     *         "allowVisitDraftViaPermalink": boolean,
+     *         "version": "",
+     *         "articleListStyle": "", // Optional values: "titleOnly"/"titleAndContent"/"titleAndAbstract"
+     *         "commentable": boolean,
+     *         "feedOutputMode: "" // Optional values: "abstract"/"full"
+     *         "feedOutputCnt": int,
+     *         "faviconURL": "",
+     *         "syncGitHub": boolean,
+     *         "pullGitHub": boolean,
+     *         "customVars" "", // 支持配置自定义参数 https://github.com/b3log/solo/issues/12535
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getPreference(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + renderer.setJSONObject(new JSONObject().put(Keys.STATUS_CODE, false)); + + return; + } + + String footerContent = ""; + final JSONObject opt = optionQueryService.getOptionById(Option.ID_C_FOOTER_CONTENT); + if (null != opt) { + footerContent = opt.optString(Option.OPTION_VALUE); + } + preference.put(Option.ID_C_FOOTER_CONTENT, footerContent); + + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + ret.put(Option.CATEGORY_C_PREFERENCE, preference); + ret.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Updates the preference by the specified request. + *

+ * Request json: + *

+     * {
+     *     "preference": {
+     *         "mostViewArticleDisplayCount": int,
+     *         "recentCommentDisplayCount": int,
+     *         "mostUsedTagDisplayCount": int,
+     *         "articleListDisplayCount": int,
+     *         "articleListPaginationWindowSize": int,
+     *         "mostCommentArticleDisplayCount": int,
+     *         "externalRelevantArticlesDisplayCount": int,
+     *         "relevantArticlesDisplayCount": int,
+     *         "randomArticlesDisplayCount": int,
+     *         "blogTitle": "",
+     *         "blogSubtitle": "",
+     *         "localeString": "",
+     *         "timeZoneId": "",
+     *         "noticeBoard": "",
+     *         "footerContent": "",
+     *         "htmlHead": "",
+     *         "metaKeywords": "",
+     *         "metaDescription": "",
+     *         "enableArticleUpdateHint": boolean,
+     *         "signs": [{
+     *             "oId": "",
+     *             "signHTML": ""
+     *             }, ...],
+     *         "allowVisitDraftViaPermalink": boolean,
+     *         "articleListStyle": "",
+     *         "commentable": boolean,
+     *         "feedOutputMode: "",
+     *         "feedOutputCnt": int,
+     *         "faviconURL": "",
+     *         "syncGitHub": boolean,
+     *         "pullGitHub": boolean,
+     *         "customVars" "", // 支持配置自定义参数 https://github.com/b3log/solo/issues/12535
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void updatePreference(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final JSONObject requestJSONObject = context.requestJSON(); + final JSONObject preference = requestJSONObject.getJSONObject(Option.CATEGORY_C_PREFERENCE); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + if (isInvalid(preference, ret)) { + return; + } + + preferenceMgmtService.updatePreference(preference); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Checks whether the specified preference is invalid and sets the specified response object. + * + * @param preference the specified preference + * @param responseObject the specified response object + * @return {@code true} if the specified preference is invalid, returns {@code false} otherwise + */ + private boolean isInvalid(final JSONObject preference, final JSONObject responseObject) { + responseObject.put(Keys.STATUS_CODE, false); + + final StringBuilder errMsgBuilder = new StringBuilder('[' + langPropsService.get("paramSettingsLabel")); + errMsgBuilder.append(" - "); + + String input = preference.optString(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("externalRelevantArticlesDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("relevantArticlesDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("randomArticlesDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("indexMostCommentArticleDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("indexMostViewArticleDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_RECENT_COMMENT_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("indexRecentCommentDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_MOST_USED_TAG_DISPLAY_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("indexTagDisplayCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("pageSizeLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("windowSizeLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + input = preference.optString(Option.ID_C_FEED_OUTPUT_CNT); + if (!isNonNegativeInteger(input)) { + errMsgBuilder.append(langPropsService.get("feedOutputCntLabel")).append("] ") + .append(langPropsService.get("nonNegativeIntegerOnlyLabel")); + responseObject.put(Keys.MSG, errMsgBuilder.toString()); + return true; + } + + return false; + } + + /** + * Checks whether the specified input is a non-negative integer. + * + * @param input the specified input + * @return {@code true} if it is, returns {@code false} otherwise + */ + private boolean isNonNegativeInteger(final String input) { + try { + return 0 <= Integer.valueOf(input); + } catch (final Exception e) { + return false; + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/RepairConsole.java b/src/main/java/org/b3log/solo/processor/console/RepairConsole.java new file mode 100644 index 00000000..29cfdd5f --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/RepairConsole.java @@ -0,0 +1,116 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.TextHtmlRenderer; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.TagArticleRepository; +import org.b3log.solo.repository.TagRepository; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.PreferenceMgmtService; +import org.b3log.solo.service.StatisticMgmtService; +import org.b3log.solo.service.StatisticQueryService; +import org.json.JSONObject; + +/** + * Provides patches on some special issues. + * + * @author Liang Ding + * @version 1.2.0.21, Mar 3, 2019 + * @since 0.3.1 + */ +@RequestProcessor +@Before(ConsoleAuthAdvice.class) +public class RepairConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(RepairConsole.class); + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Preference management service. + */ + @Inject + private PreferenceMgmtService preferenceMgmtService; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Statistic query service. + */ + @Inject + private StatisticQueryService statisticQueryService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Restores the signs of preference to default. + * + * @param context the specified context + */ + public void restoreSigns(final RequestContext context) { + final TextHtmlRenderer renderer = new TextHtmlRenderer(); + context.setRenderer(renderer); + + try { + final JSONObject preference = optionQueryService.getPreference(); + preference.put(Option.ID_C_SIGNS, Option.DefaultPreference.DEFAULT_SIGNS); + preferenceMgmtService.updatePreference(preference); + + renderer.setContent("Restore signs succeeded."); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + renderer.setContent("Restores signs failed, error msg [" + e.getMessage() + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/SkinConsole.java b/src/main/java/org/b3log/solo/processor/console/SkinConsole.java new file mode 100644 index 00000000..4f997b10 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/SkinConsole.java @@ -0,0 +1,200 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.service.OptionQueryService; +import org.b3log.solo.service.SkinMgmtService; +import org.b3log.solo.util.Skins; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.util.Set; + +/** + * Skin console request processing. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 29, 2019 + * @since 3.5.0 + */ +@RequestProcessor +@Before(ConsoleAdminAuthAdvice.class) +public class SkinConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SkinConsole.class); + + /** + * Skin management service. + */ + @Inject + private SkinMgmtService skinMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Gets skin. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "skin": {
+     *         "skinDirName": "",
+     *         "mobileSkinDirName": "",
+     *         "skins": "[{
+     *             "skinDirName": ""
+     *         }, ....]"
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void getSkin(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final JSONObject skin = optionQueryService.getSkin(); + if (null == skin) { + renderer.setJSONObject(new JSONObject().put(Keys.STATUS_CODE, false)); + + return; + } + + final Set skinDirNames = Skins.getSkinDirNames(); + final JSONArray skinArray = new JSONArray(); + for (final String dirName : skinDirNames) { + final JSONObject s = new JSONObject(); + final String name = Latkes.getSkinName(dirName); + if (null == name) { + LOGGER.log(Level.WARN, "The directory [{0}] does not contain any skin, ignored it", dirName); + + continue; + } + + s.put(Option.ID_C_SKIN_DIR_NAME, dirName); + skinArray.put(s); + } + skin.put("skins", skinArray.toString()); + + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + ret.put(Option.CATEGORY_C_SKIN, skin); + ret.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Updates the skin by the specified request. + *

+ * Request json: + *

+     * {
+     *     "skin": {
+     *         "skinDirName": "",
+     *         "mobileSkinDirName": "",
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + public void updateSkin(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final JSONObject requestJSONObject = context.requestJSON(); + final JSONObject skin = requestJSONObject.getJSONObject(Option.CATEGORY_C_SKIN); + final JSONObject ret = new JSONObject(); + renderer.setJSONObject(ret); + + skinMgmtService.updateSkin(skin); + + final HttpServletResponse response = context.getResponse(); + final Cookie skinDirNameCookie = new Cookie(Common.COOKIE_NAME_SKIN, skin.getString(Option.ID_C_SKIN_DIR_NAME)); + skinDirNameCookie.setMaxAge(60 * 60); // 1 hour + skinDirNameCookie.setPath("/"); + response.addCookie(skinDirNameCookie); + final Cookie mobileSkinDirNameCookie = new Cookie(Common.COOKIE_NAME_MOBILE_SKIN, skin.getString(Option.ID_C_MOBILE_SKIN_DIR_NAME)); + mobileSkinDirNameCookie.setMaxAge(60 * 60); // 1 hour + mobileSkinDirNameCookie.setPath("/"); + response.addCookie(mobileSkinDirNameCookie); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Checks whether the specified input is a non-negative integer. + * + * @param input the specified input + * @return {@code true} if it is, returns {@code false} otherwise + */ + private boolean isNonNegativeInteger(final String input) { + try { + return 0 <= Integer.valueOf(input); + } catch (final Exception e) { + return false; + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/TagConsole.java b/src/main/java/org/b3log/solo/processor/console/TagConsole.java new file mode 100644 index 00000000..e7c2eafa --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/TagConsole.java @@ -0,0 +1,139 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Tag; +import org.b3log.solo.service.TagMgmtService; +import org.b3log.solo.service.TagQueryService; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tag console request processing. + * + * @author Liang Ding + * @version 1.0.0.3, Dec 11, 2018 + * @since 0.4.0 + */ +@RequestProcessor +public class TagConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(TagConsole.class); + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * Gets all tags. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "tags": [
+     *         {"tagTitle": "", ....},
+     *         ....
+     *     ]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAuthAdvice.class) + public void getTags(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + try { + jsonObject.put(Tag.TAGS, tagQueryService.getTags()); + jsonObject.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets tags failed", e); + + jsonObject.put(Keys.STATUS_CODE, false); + } + } + + /** + * Gets all unused tags. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "unusedTags": [
+     *         {"tagTitle": "", ....},
+     *         ....
+     *     ]
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void getUnusedTags(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + + final List unusedTags = new ArrayList<>(); + + try { + jsonObject.put(Common.UNUSED_TAGS, unusedTags); + + final List tags = tagQueryService.getTags(); + for (int i = 0; i < tags.size(); i++) { + final JSONObject tag = tags.get(i); + final String tagId = tag.optString(Keys.OBJECT_ID); + final int articleCount = tagQueryService.getArticleCount(tagId); + if (1 > articleCount) { + unusedTags.add(tag); + } + } + + jsonObject.put(Keys.STATUS_CODE, true); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets unused tags failed", e); + + jsonObject.put(Keys.STATUS_CODE, false); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/UserConsole.java b/src/main/java/org/b3log/solo/processor/console/UserConsole.java new file mode 100644 index 00000000..bd759064 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/UserConsole.java @@ -0,0 +1,283 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringEscapeUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.User; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.servlet.annotation.Before; +import org.b3log.latke.servlet.annotation.RequestProcessor; +import org.b3log.latke.servlet.renderer.JsonRenderer; +import org.b3log.solo.service.UserMgmtService; +import org.b3log.solo.service.UserQueryService; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * User console request processing. + * + * @author Liang Ding + * @author DASHU + * @version 1.2.1.7, Mar 29, 2019 + * @since 0.4.0 + */ +@RequestProcessor +public class UserConsole { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(UserConsole.class); + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Updates a user by the specified request. + * + *

+ * Request json: + *

+     * {
+     *     "oId": "",
+     *     "userName": "",
+     *     "userRole": "",
+     *     "userURL": "",
+     *     "userAvatar": "",
+     *     "userB3Key": ""
+     * }
+     * 
+ *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void updateUser(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject ret = new JSONObject(); + + try { + final JSONObject requestJSONObject = context.requestJSON(); + userMgmtService.updateUser(requestJSONObject); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + renderer.setJSONObject(ret); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("updateFailLabel")); + } + } + + /** + * Removes a user by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void removeUser(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + try { + final String userId = context.pathVar("id"); + userMgmtService.removeUser(userId); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("removeSuccLabel")); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } + + /** + * Gets users by the specified request json object. + *

+ * The request URI contains the pagination arguments. For example, the request URI is /console/users/1/10/20, means + * the current page is 1, the page size is 10, and the window size is 20. + *

+ *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "users": [{
+     *         "oId": "",
+     *         "userName": "",
+     *         "roleName": "",
+     *         ....
+     *      }, ....]
+     *     "sc": true
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void getUsers(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + + try { + final String requestURI = context.requestURI(); + final String path = requestURI.substring((Latkes.getContextPath() + "/console/users/").length()); + final JSONObject requestJSONObject = Solos.buildPaginationRequest(path); + final JSONObject result = userQueryService.getUsers(requestJSONObject); + result.put(Keys.STATUS_CODE, true); + renderer.setJSONObject(result); + + final JSONArray users = result.optJSONArray(User.USERS); + for (int i = 0; i < users.length(); i++) { + final JSONObject user = users.optJSONObject(i); + String userName = user.optString(User.USER_NAME); + userName = StringEscapeUtils.escapeXml(userName); + user.put(User.USER_NAME, userName); + } + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + } + } + + /** + * Gets a user by the specified request. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "user": {
+     *         "oId": "",
+     *         "userName": "",
+     *         "userAvatar": ""
+     *     }
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void getUser(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final String userId = context.pathVar("id"); + + final JSONObject result = userQueryService.getUser(userId); + if (null == result) { + final JSONObject jsonObject = new JSONObject().put(Keys.STATUS_CODE, false); + renderer.setJSONObject(jsonObject); + jsonObject.put(Keys.MSG, langPropsService.get("getFailLabel")); + + return; + } + + renderer.setJSONObject(result); + result.put(Keys.STATUS_CODE, true); + } + + /** + * Change a user role. + *

+ * Renders the response with a json object, for example, + *

+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ *

+ * + * @param context the specified request context + */ + @Before(ConsoleAdminAuthAdvice.class) + public void changeUserRole(final RequestContext context) { + final JsonRenderer renderer = new JsonRenderer(); + context.setRenderer(renderer); + final JSONObject jsonObject = new JSONObject(); + renderer.setJSONObject(jsonObject); + try { + final String userId = context.pathVar("id"); + userMgmtService.changeRole(userId); + + jsonObject.put(Keys.STATUS_CODE, true); + jsonObject.put(Keys.MSG, langPropsService.get("updateSuccLabel")); + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + jsonObject.put(Keys.STATUS_CODE, false); + jsonObject.put(Keys.MSG, langPropsService.get("removeFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/processor/console/package-info.java b/src/main/java/org/b3log/solo/processor/console/package-info.java new file mode 100644 index 00000000..32585efb --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/console/package-info.java @@ -0,0 +1,4 @@ +/** + * Console requests (Articles, Comments, Preference, etc, management) processing. + */ +package org.b3log.solo.processor.console; diff --git a/src/main/java/org/b3log/solo/processor/package-info.java b/src/main/java/org/b3log/solo/processor/package-info.java new file mode 100644 index 00000000..22b259e6 --- /dev/null +++ b/src/main/java/org/b3log/solo/processor/package-info.java @@ -0,0 +1,4 @@ +/** + * HTTP request processing. + */ +package org.b3log.solo.processor; diff --git a/src/main/java/org/b3log/solo/repository/ArchiveDateArticleRepository.java b/src/main/java/org/b3log/solo/repository/ArchiveDateArticleRepository.java new file mode 100644 index 00000000..bd258056 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/ArchiveDateArticleRepository.java @@ -0,0 +1,135 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.ArchiveDate; +import org.b3log.solo.model.Article; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * Archive date-Article repository. + * + * @author Liang Ding + * @version 1.1.0.0, Sep 11, 2019 + * @since 0.3.1 + */ +@Repository +public class ArchiveDateArticleRepository extends AbstractRepository { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArchiveDateArticleRepository.class); + + /** + * Public constructor. + */ + public ArchiveDateArticleRepository() { + super((ArchiveDate.ARCHIVE_DATE + "_" + Article.ARTICLE).toLowerCase()); + } + + /** + * Gets published article count of an archive date specified by the given archive data id. + * + * @param archiveDateId the given archive date id + * @return published article count, returns {@code -1} if occurred an exception + */ + public int getPublishedArticleCount(final String archiveDateId) { + try { + final BeanManager beanManager = BeanManager.getInstance(); + final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class); + final ArchiveDateArticleRepository archiveDateArticleRepository = beanManager.getReference(ArchiveDateArticleRepository.class); + + final StringBuilder queryCount = new StringBuilder("SELECT count(DISTINCT(article.oId)) as C FROM "); + final StringBuilder queryStr = new StringBuilder(articleRepository.getName() + " AS article,"). + append(archiveDateArticleRepository.getName() + " AS archive_article"). + append(" WHERE article.oId=archive_article.article_oId "). + append(" AND article.articleStatus=").append(Article.ARTICLE_STATUS_C_PUBLISHED). + append(" AND ").append("archive_article.archiveDate_oId=").append(archiveDateId); + final List articlesCountResult = select(queryCount.append(queryStr.toString()).toString()); + return articlesCountResult == null ? 0 : articlesCountResult.get(0).optInt("C"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets archivedate [" + archiveDateId + "]'s published article count failed", e); + + return -1; + } + } + + /** + * Gets archive date-article relations by the specified archive date id. + * + * @param archiveDateId the specified archive date id + * @param currentPageNum the specified current page number, MUST greater then {@code 0} + * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0} + * @return for example + *
+     * {
+     *     "pagination": {
+     *       "paginationPageCount": 88250
+     *     },
+     *     "rslts": [{
+     *         "oId": "",
+     *         "archiveDate_oId": "",
+     *         "article_oId": ""
+     *     }, ....]
+     * }
+     * 
+ * @throws RepositoryException repository exception + */ + public JSONObject getByArchiveDateId(final String archiveDateId, final int currentPageNum, final int pageSize) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(ArchiveDate.ARCHIVE_DATE + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, archiveDateId)). + addSort(Article.ARTICLE + "_" + Keys.OBJECT_ID, SortDirection.DESCENDING). + setPage(currentPageNum, pageSize).setPageCount(1); + + return get(query); + } + + /** + * Gets an archive date-article relations by the specified article id. + * + * @param articleId the specified article id + * @return for example + *
+     * {
+     *     "archiveDate_oId": "",
+     *     "article_oId": articleId
+     * }, returns {@code null} if not found
+     * 
+ * @throws RepositoryException repository exception + */ + public JSONObject getByArticleId(final String articleId) throws RepositoryException { + final Query query = new Query(). + setFilter(new PropertyFilter(Article.ARTICLE + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, articleId)); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } +} diff --git a/src/main/java/org/b3log/solo/repository/ArchiveDateRepository.java b/src/main/java/org/b3log/solo/repository/ArchiveDateRepository.java new file mode 100644 index 00000000..af770d4a --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/ArchiveDateRepository.java @@ -0,0 +1,122 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.apache.commons.lang.time.DateUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.ArchiveDate; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.text.ParseException; +import java.util.List; + +/** + * Archive date repository. + * + * @author Liang Ding + * @version 1.0.0.5, Sep 11, 2019 + * @since 0.3.1 + */ +@Repository +public class ArchiveDateRepository extends AbstractRepository { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArchiveDateRepository.class); + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + /** + * Public constructor. + */ + public ArchiveDateRepository() { + super(ArchiveDate.ARCHIVE_DATE.toLowerCase()); + } + + /** + * Gets an archive date by the specified archive date string. + * + * @param archiveDate the specified archive date stirng (yyyy/MM) + * @return an archive date, {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByArchiveDate(final String archiveDate) throws RepositoryException { + long time; + try { + time = DateUtils.parseDate(archiveDate, new String[]{"yyyy/MM"}).getTime(); + } catch (final ParseException e) { + return null; + } + + LOGGER.log(Level.TRACE, "Archive date [{0}] parsed to time [{1}]", archiveDate, time); + + Query query = new Query().setFilter(new PropertyFilter(ArchiveDate.ARCHIVE_TIME, FilterOperator.EQUAL, time)).setPageCount(1); + JSONObject result = get(query); + JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + // Try to fix wired timezone issue: https://github.com/b3log/solo/issues/12435 + try { + time = DateUtils.parseDate(archiveDate, new String[]{"yyyy/MM"}).getTime(); + time += 60 * 1000 * 60 * 8; + } catch (final ParseException e) { + return null; + } + + LOGGER.log(Level.TRACE, "Fix archive date [{0}] parsed to time [{1}]", archiveDate, time); + + query = new Query().setFilter(new PropertyFilter(ArchiveDate.ARCHIVE_TIME, FilterOperator.EQUAL, time)).setPageCount(1); + result = get(query); + array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + } + + return array.optJSONObject(0); + } + + /** + * Get archive dates. + * + * @return a list of archive date, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getArchiveDates() throws RepositoryException { + final Query query = new Query().addSort(ArchiveDate.ARCHIVE_TIME, SortDirection.DESCENDING).setPageCount(1); + // TODO: Performance issue + final List ret = getList(query); + for (final JSONObject archiveDate : ret) { + final String archiveDateId = archiveDate.optString(Keys.OBJECT_ID); + final int publishedArticleCount = archiveDateArticleRepository.getPublishedArticleCount(archiveDateId); + archiveDate.put(ArchiveDate.ARCHIVE_DATE_T_PUBLISHED_ARTICLE_COUNT, publishedArticleCount); + } + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/repository/ArticleRepository.java b/src/main/java/org/b3log/solo/repository/ArticleRepository.java new file mode 100644 index 00000000..547c0301 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/ArticleRepository.java @@ -0,0 +1,365 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.cache.ArticleCache; +import org.b3log.solo.model.Article; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Article repository. + * + * @author Liang Ding + * @version 1.1.1.13, Jun 6, 2019 + * @since 0.3.1 + */ +@Repository +public class ArticleRepository extends AbstractRepository { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleRepository.class); + + /** + * Random range. + */ + private static final double RANDOM_RANGE = 0.1D; + + /** + * Article cache. + */ + @Inject + private ArticleCache articleCache; + + /** + * Public constructor. + */ + public ArticleRepository() { + super(Article.ARTICLE); + } + + @Override + public void remove(final String id) throws RepositoryException { + super.remove(id); + + articleCache.removeArticle(id); + } + + @Override + public JSONObject get(final String id) throws RepositoryException { + JSONObject ret = articleCache.getArticle(id); + if (null != ret) { + return ret; + } + + ret = super.get(id); + if (null == ret) { + return null; + } + + articleCache.putArticle(ret); + + return ret; + } + + @Override + public void update(final String id, final JSONObject article, final String... propertyNames) throws RepositoryException { + super.update(id, article, propertyNames); + + article.put(Keys.OBJECT_ID, id); + articleCache.putArticle(article); + } + + @Override + public List getRandomly(final int fetchSize) throws RepositoryException { + final List ret = new ArrayList<>(); + + if (0 == count()) { + return ret; + } + + final double mid = Math.random() + RANDOM_RANGE; + + LOGGER.log(Level.TRACE, "Random mid[{0}]", mid); + + Query query = new Query().setFilter(CompositeFilterOperator.and( + new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.GREATER_THAN_OR_EQUAL, mid), + new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.LESS_THAN_OR_EQUAL, mid), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + setPage(1, fetchSize).setPageCount(1); + + final List list1 = getList(query); + ret.addAll(list1); + + final int reminingSize = fetchSize - list1.size(); + if (0 != reminingSize) { // Query for remains + query = new Query(); + query.setFilter( + CompositeFilterOperator.and( + new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.GREATER_THAN_OR_EQUAL, 0D), + new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.LESS_THAN_OR_EQUAL, mid), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + setPage(1, reminingSize).setPageCount(1); + + final List list2 = getList(query); + + ret.addAll(list2); + } + + return ret; + } + + /** + * Gets published articles by the specified author id, current page number and page size. + * + * @param authorId the specified author id + * @param currentPageNum the specified current page number, MUST greater then {@code 0} + * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0} + * @return for example + *
+     * {
+     *     "pagination": {
+     *       "paginationPageCount": 88250
+     *     },
+     *     "rslts": [{
+     *         // article keys....
+     *     }, ....]
+     * }
+     * 
+ * @throws RepositoryException repository exception + */ + public JSONObject getByAuthorId(final String authorId, final int currentPageNum, final int pageSize) throws RepositoryException { + final Query query = new Query(). + setFilter(CompositeFilterOperator.and( + new PropertyFilter(Article.ARTICLE_AUTHOR_ID, FilterOperator.EQUAL, authorId), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING).addSort(Article.ARTICLE_PUT_TOP, SortDirection.DESCENDING). + setPage(currentPageNum, pageSize); + + return get(query); + } + + /** + * Gets an article by the specified permalink. + * + * @param permalink the specified permalink + * @return an article, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByPermalink(final String permalink) throws RepositoryException { + JSONObject ret = articleCache.getArticleByPermalink(permalink); + if (null != ret) { + return ret; + } + + final Query query = new Query(). + setFilter(new PropertyFilter(Article.ARTICLE_PERMALINK, FilterOperator.EQUAL, permalink)). + setPageCount(1); + + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + ret = array.optJSONObject(0); + articleCache.putArticle(ret); + + return ret; + } + + /** + * Gets post articles recently with the specified fetch size. + * + * @param fetchSize the specified fetch size + * @return a list of articles recently, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getRecentArticles(final int fetchSize) throws RepositoryException { + final Query query = new Query(). + setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING). + setPage(1, fetchSize).setPageCount(1); + + return getList(query); + } + + /** + * Gets most commented and published articles with the specified number. + * + * @param num the specified number + * @return a list of most comment articles, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getMostCommentArticles(final int num) throws RepositoryException { + final Query query = new Query(). + addSort(Article.ARTICLE_COMMENT_COUNT, SortDirection.DESCENDING). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING). + setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)). + setPage(1, num).setPageCount(1); + + return getList(query); + } + + /** + * Gets most view count and published articles with the specified number. + * + * @param num the specified number + * @return a list of most view count articles, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getMostViewCountArticles(final int num) throws RepositoryException { + final Query query = new Query(). + addSort(Article.ARTICLE_VIEW_COUNT, SortDirection.DESCENDING). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING). + setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)). + setPage(1, num).setPageCount(1); + + return getList(query); + } + + /** + * Gets the previous article(by create date) by the specified article id. + * + * @param articleId the specified article id + * @return the previous article, + *
+     * {
+     *     "articleTitle": "",
+     *     "articlePermalink": "",
+     *     "articleAbstract: ""
+     * }
+     * 
+ * returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getPreviousArticle(final String articleId) throws RepositoryException { + final JSONObject currentArticle = get(articleId); + if (null == currentArticle) { + return null; + } + + final long currentArticleCreated = currentArticle.optLong(Article.ARTICLE_CREATED); + + final Query query = new Query(). + setFilter(CompositeFilterOperator.and( + new PropertyFilter(Article.ARTICLE_CREATED, FilterOperator.LESS_THAN, currentArticleCreated), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + addSort(Article.ARTICLE_CREATED, SortDirection.DESCENDING). + setPage(1, 1).setPageCount(1). + select(Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK, Article.ARTICLE_ABSTRACT); + + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + final JSONObject ret = new JSONObject(); + final JSONObject article = array.optJSONObject(0); + + try { + ret.put(Article.ARTICLE_TITLE, article.getString(Article.ARTICLE_TITLE)); + ret.put(Article.ARTICLE_PERMALINK, article.getString(Article.ARTICLE_PERMALINK)); + ret.put(Article.ARTICLE_ABSTRACT, article.getString((Article.ARTICLE_ABSTRACT))); + } catch (final JSONException e) { + throw new RepositoryException(e); + } + + return ret; + } + + /** + * Gets the next article(by create date, oId) by the specified article id. + * + * @param articleId the specified article id + * @return the next article, + *
+     * {
+     *     "articleTitle": "",
+     *     "articlePermalink": "",
+     *     "articleAbstract: ""
+     * }
+     * 
+ * returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getNextArticle(final String articleId) throws RepositoryException { + final JSONObject currentArticle = get(articleId); + if (null == currentArticle) { + return null; + } + + final long currentArticleCreated = currentArticle.optLong(Article.ARTICLE_CREATED); + + final Query query = new Query(). + setFilter(CompositeFilterOperator.and( + new PropertyFilter(Article.ARTICLE_CREATED, FilterOperator.GREATER_THAN, currentArticleCreated), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + addSort(Article.ARTICLE_CREATED, SortDirection.ASCENDING). + setPage(1, 1).setPageCount(1). + select(Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK, Article.ARTICLE_ABSTRACT); + + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + final JSONObject ret = new JSONObject(); + final JSONObject article = array.optJSONObject(0); + + try { + ret.put(Article.ARTICLE_TITLE, article.getString(Article.ARTICLE_TITLE)); + ret.put(Article.ARTICLE_PERMALINK, article.getString(Article.ARTICLE_PERMALINK)); + ret.put(Article.ARTICLE_ABSTRACT, article.getString((Article.ARTICLE_ABSTRACT))); + } catch (final JSONException e) { + throw new RepositoryException(e); + } + + return ret; + } + + /** + * Determines an article specified by the given article id is published. + * + * @param articleId the given article id + * @return {@code true} if it is published, {@code false} otherwise + * @throws RepositoryException repository exception + */ + public boolean isPublished(final String articleId) throws RepositoryException { + final JSONObject article = get(articleId); + if (null == article) { + return false; + } + + return Article.ARTICLE_STATUS_C_PUBLISHED == article.optInt(Article.ARTICLE_STATUS); + } +} diff --git a/src/main/java/org/b3log/solo/repository/CategoryRepository.java b/src/main/java/org/b3log/solo/repository/CategoryRepository.java new file mode 100644 index 00000000..fceeb9c2 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/CategoryRepository.java @@ -0,0 +1,201 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Category; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * Category repository. + * + * @author Liang Ding + * @version 1.2.0.3, Sep 11, 2019 + * @since 2.0.0 + */ +@Repository +public class CategoryRepository extends AbstractRepository { + + /** + * Public constructor. + */ + public CategoryRepository() { + super(Category.CATEGORY); + } + + /** + * Gets a category by the specified category title. + * + * @param categoryTitle the specified category title + * @return a category, {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByTitle(final String categoryTitle) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY_TITLE, FilterOperator.EQUAL, categoryTitle)).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets a category by the specified category URI. + * + * @param categoryURI the specified category URI + * @return a category, {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByURI(final String categoryURI) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY_URI, FilterOperator.EQUAL, categoryURI)).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets the maximum order. + * + * @return order number, returns {@code -1} if not found + * @throws RepositoryException repository exception + */ + public int getMaxOrder() throws RepositoryException { + final Query query = new Query().addSort(Category.CATEGORY_ORDER, SortDirection.DESCENDING); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return -1; + } + + return array.optJSONObject(0).optInt(Category.CATEGORY_ORDER); + } + + /** + * Gets the upper category of the category specified by the given id. + * + * @param id the given id + * @return upper category, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getUpper(final String id) throws RepositoryException { + final JSONObject category = get(id); + if (null == category) { + return null; + } + + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY_ORDER, FilterOperator.LESS_THAN, category.optInt(Category.CATEGORY_ORDER))). + addSort(Category.CATEGORY_ORDER, SortDirection.DESCENDING).setPage(1, 1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets the under category of the category specified by the given id. + * + * @param id the given id + * @return under category, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getUnder(final String id) throws RepositoryException { + final JSONObject category = get(id); + if (null == category) { + return null; + } + + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY_ORDER, FilterOperator.GREATER_THAN, category.optInt(Category.CATEGORY_ORDER))). + addSort(Category.CATEGORY_ORDER, SortDirection.ASCENDING).setPage(1, 1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets a category by the specified order. + * + * @param order the specified order + * @return category, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByOrder(final int order) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY_ORDER, FilterOperator.EQUAL, order)); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets most used categories (contains the most tags) with the specified number. + * + * @param num the specified number + * @return a list of most used categories, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getMostUsedCategories(final int num) throws RepositoryException { + final Query query = new Query().addSort(Category.CATEGORY_ORDER, SortDirection.ASCENDING). + setPage(1, num).setPageCount(1); + final List ret = getList(query); + + final BeanManager beanManager = BeanManager.getInstance(); + final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class); + final TagArticleRepository tagArticleRepository = beanManager.getReference(TagArticleRepository.class); + final CategoryTagRepository categoryTagRepository = beanManager.getReference(CategoryTagRepository.class); + + for (final JSONObject category : ret) { + final String categoryId = category.optString(Keys.OBJECT_ID); + final StringBuilder queryCount = new StringBuilder("SELECT count(DISTINCT(b3_solo_article.oId)) as C FROM "); + final StringBuilder queryStr = new StringBuilder(articleRepository.getName() + " AS b3_solo_article,"). + append(tagArticleRepository.getName() + " AS b3_solo_tag_article"). + append(" WHERE b3_solo_article.oId=b3_solo_tag_article.article_oId "). + append(" AND b3_solo_article.articleStatus=").append(Article.ARTICLE_STATUS_C_PUBLISHED). + append(" AND ").append("b3_solo_tag_article.tag_oId").append(" IN ("). + append("SELECT tag_oId FROM ").append(categoryTagRepository.getName() + " AS b3_solo_category_tag WHERE b3_solo_category_tag.category_oId = "). + append(categoryId).append(")"); + final List articlesCountResult = select(queryCount.append(queryStr.toString()).toString()); + final int articleCount = articlesCountResult == null ? 0 : articlesCountResult.get(0).optInt("C"); + category.put(Category.CATEGORY_T_PUBLISHED_ARTICLE_COUNT, articleCount); + } + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/repository/CategoryTagRepository.java b/src/main/java/org/b3log/solo/repository/CategoryTagRepository.java new file mode 100644 index 00000000..5e759dae --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/CategoryTagRepository.java @@ -0,0 +1,128 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Tag; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Category-Tag relation repository. + * + * @author Liang Ding + * @version 1.1.0.3, Jan 15, 2019 + * @since 2.0.0 + */ +@Repository +public class CategoryTagRepository extends AbstractRepository { + + /** + * Public constructor. + */ + public CategoryTagRepository() { + super(Category.CATEGORY + "_" + Tag.TAG); + } + + /** + * Gets category-tag relations by the specified category id. + * + * @param categoryId the specified category id + * @param currentPageNum the specified current page number, MUST greater then {@code 0} + * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0} + * @return for example
+     * {
+     *     "pagination": {
+     *       "paginationPageCount": 88250
+     *     },
+     *     "rslts": [{
+     *         "oId": "",
+     *         "category_oId": categoryId,
+     *         "tag_oId": ""
+     *     }, ....]
+     * }
+     * 
+ * @throws RepositoryException repository exception + */ + public JSONObject getByCategoryId(final String categoryId, final int currentPageNum, final int pageSize) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, categoryId)). + setPage(currentPageNum, pageSize).setPageCount(1); + + return get(query); + } + + /** + * Gets category-tag relations by the specified tag id. + * + * @param tagId the specified tag id + * @param currentPageNum the specified current page number, MUST greater then {@code 0} + * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0} + * @return for example
+     * {
+     *     "pagination": {
+     *       "paginationPageCount": 88250
+     *     },
+     *     "rslts": [{
+     *         "oId": "",
+     *         "category_oId": "",
+     *         "tag_oId": tagId
+     *     }, ....]
+     * }
+     * 
+ * @throws RepositoryException repository exception + */ + public JSONObject getByTagId(final String tagId, final int currentPageNum, final int pageSize) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)). + setPage(currentPageNum, pageSize).setPageCount(1); + + return get(query); + } + + /** + * Removes category-tag relations by the specified category id. + * + * @param categoryId the specified category id + * @throws RepositoryException repository exception + */ + public void removeByCategoryId(final String categoryId) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Category.CATEGORY + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, categoryId)); + final JSONArray relations = get(query).optJSONArray(Keys.RESULTS); + for (int i = 0; i < relations.length(); i++) { + final JSONObject rel = relations.optJSONObject(i); + remove(rel.optString(Keys.OBJECT_ID)); + } + } + + /** + * Removes category-tag relations by the specified tag id. + * + * @param tagId the specified tag id + * @throws RepositoryException repository exception + */ + public void removeByTagId(final String tagId) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)); + final JSONArray relations = get(query).optJSONArray(Keys.RESULTS); + for (int i = 0; i < relations.length(); i++) { + final JSONObject rel = relations.optJSONObject(i); + remove(rel.optString(Keys.OBJECT_ID)); + } + } +} diff --git a/src/main/java/org/b3log/solo/repository/CommentRepository.java b/src/main/java/org/b3log/solo/repository/CommentRepository.java new file mode 100644 index 00000000..61097de7 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/CommentRepository.java @@ -0,0 +1,175 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.cache.CommentCache; +import org.b3log.solo.model.Comment; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.List; + +/** + * Comment repository. + * + * @author Liang Ding + * @version 1.0.0.6, Jan 15, 2019 + * @since 0.3.1 + */ +@Repository +public class CommentRepository extends AbstractRepository { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CommentRepository.class); + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Comment cache. + */ + @Inject + private CommentCache commentCache; + + /** + * Public constructor. + */ + public CommentRepository() { + super(Comment.COMMENT); + } + + @Override + public void remove(final String id) throws RepositoryException { + super.remove(id); + + commentCache.removeComment(id); + } + + @Override + public JSONObject get(final String id) throws RepositoryException { + JSONObject ret = commentCache.getComment(id); + if (null != ret) { + return ret; + } + + ret = super.get(id); + if (null == ret) { + return null; + } + + commentCache.putComment(ret); + + return ret; + } + + @Override + public void update(final String id, final JSONObject comment, final String... propertyNames) throws RepositoryException { + super.update(id, comment, propertyNames); + + comment.put(Keys.OBJECT_ID, id); + commentCache.putComment(comment); + } + + /** + * Gets post comments recently with the specified fetch. + * + * @param fetchSize the specified fetch size + * @return a list of comments recently, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getRecentComments(final int fetchSize) throws RepositoryException { + final Query query = new Query(). + addSort(Keys.OBJECT_ID, SortDirection.DESCENDING). + setPage(1, fetchSize).setPageCount(1); + final List ret = getList(query); + // Removes unpublished article related comments + removeForUnpublishedArticles(ret); + + return ret; + } + + /** + * Gets comments with the specified on id, current page number and + * page size. + * + * @param onId the specified on id + * @param currentPageNum the specified current page number + * @param pageSize the specified page size + * @return a list of comments, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getComments(final String onId, final int currentPageNum, final int pageSize) throws RepositoryException { + final Query query = new Query(). + addSort(Keys.OBJECT_ID, SortDirection.DESCENDING). + setFilter(new PropertyFilter(Comment.COMMENT_ON_ID, FilterOperator.EQUAL, onId)). + setPage(currentPageNum, pageSize).setPageCount(1); + + return getList(query); + } + + /** + * Removes comments with the specified on id. + * + * @param onId the specified on id + * @return removed count + * @throws RepositoryException repository exception + */ + public int removeComments(final String onId) throws RepositoryException { + final List comments = getComments(onId, 1, Integer.MAX_VALUE); + for (final JSONObject comment : comments) { + final String commentId = comment.optString(Keys.OBJECT_ID); + remove(commentId); + } + + LOGGER.log(Level.DEBUG, "Removed comments[onId={0}, removedCnt={1}]", onId, comments.size()); + + return comments.size(); + } + + /** + * Removes comments of unpublished articles for the specified comments. + * + * @param comments the specified comments + * @throws RepositoryException repository exception + */ + private void removeForUnpublishedArticles(final List comments) throws RepositoryException { + LOGGER.debug("Removing unpublished articles' comments...."); + final Iterator iterator = comments.iterator(); + + while (iterator.hasNext()) { + final JSONObject comment = iterator.next(); + final String articleId = comment.optString(Comment.COMMENT_ON_ID); + if (!articleRepository.isPublished(articleId)) { + iterator.remove(); + } + } + + LOGGER.debug("Removed unpublished articles' comments...."); + } +} diff --git a/src/main/java/org/b3log/solo/repository/LinkRepository.java b/src/main/java/org/b3log/solo/repository/LinkRepository.java new file mode 100644 index 00000000..f1fa6489 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/LinkRepository.java @@ -0,0 +1,144 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.Link; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Link repository. + * + * @author Liang Ding + * @version 1.0.0.5, Jan 15, 2019 + * @since 0.3.1 + */ +@Repository +public class LinkRepository extends AbstractRepository { + + /** + * Public constructor. + */ + public LinkRepository() { + super(Link.LINK); + } + + /** + * Gets a link by the specified address. + * + * @param address the specified address + * @return link, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByAddress(final String address) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Link.LINK_ADDRESS, FilterOperator.EQUAL, address)).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets the maximum order. + * + * @return order number, returns {@code -1} if not found + * @throws RepositoryException repository exception + */ + public int getMaxOrder() throws RepositoryException { + final Query query = new Query().addSort(Link.LINK_ORDER, SortDirection.DESCENDING); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return -1; + } + + return array.optJSONObject(0).optInt(Link.LINK_ORDER); + } + + /** + * Gets the upper link of the link specified by the given id. + * + * @param id the given id + * @return upper link, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getUpper(final String id) throws RepositoryException { + final JSONObject link = get(id); + if (null == link) { + return null; + } + + final Query query = new Query().setFilter(new PropertyFilter(Link.LINK_ORDER, FilterOperator.LESS_THAN, link.optInt(Link.LINK_ORDER))). + addSort(Link.LINK_ORDER, SortDirection.DESCENDING).setPage(1, 1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets the under link of the link specified by the given id. + * + * @param id the given id + * @return under link, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getUnder(final String id) throws RepositoryException { + final JSONObject link = get(id); + if (null == link) { + return null; + } + + final Query query = new Query().setFilter(new PropertyFilter(Link.LINK_ORDER, FilterOperator.GREATER_THAN, link.optInt(Link.LINK_ORDER))). + addSort(Link.LINK_ORDER, SortDirection.ASCENDING).setPage(1, 1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets a link by the specified order. + * + * @param order the specified order + * @return link, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByOrder(final int order) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Link.LINK_ORDER, FilterOperator.EQUAL, order)); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } +} diff --git a/src/main/java/org/b3log/solo/repository/OptionRepository.java b/src/main/java/org/b3log/solo/repository/OptionRepository.java new file mode 100644 index 00000000..e18fa206 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/OptionRepository.java @@ -0,0 +1,129 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.cache.OptionCache; +import org.b3log.solo.model.Option; +import org.json.JSONObject; + +import java.util.List; + +/** + * Option repository. + * + * @author Liang Ding + * @version 1.1.0.2, Jun 6, 2019 + * @since 0.6.0 + */ +@Repository +public class OptionRepository extends AbstractRepository { + + /** + * Option cache. + */ + @Inject + private OptionCache optionCache; + + /** + * Public constructor. + */ + public OptionRepository() { + super(Option.OPTION); + } + + @Override + public void remove(final String id) throws RepositoryException { + final JSONObject option = get(id); + if (null == option) { + return; + } + + super.remove(id); + optionCache.removeOption(id); + + final String category = option.optString(Option.OPTION_CATEGORY); + optionCache.removeCategory(category); + } + + @Override + public JSONObject get(final String id) throws RepositoryException { + JSONObject ret = optionCache.getOption(id); + if (null != ret) { + return ret; + } + + ret = super.get(id); + if (null == ret) { + return null; + } + + optionCache.putOption(ret); + + return ret; + } + + @Override + public void update(final String id, final JSONObject option, final String... propertyNames) throws RepositoryException { + super.update(id, option, propertyNames); + + option.put(Keys.OBJECT_ID, id); + optionCache.putOption(option); + } + + /** + * Gets options with the specified category. + *

+ * All options with the specified category will be merged into one json object as the return value. + *

+ * + * @param category the specified category + * @return all options with the specified category, for example, + *
+     * {
+     *     "${optionId}": "${optionValue}",
+     *     ....
+     * }
+     * 
, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getOptions(final String category) throws RepositoryException { + final JSONObject cached = optionCache.getCategory(category); + if (null != cached) { + return cached; + } + + final JSONObject ret = new JSONObject(); + try { + final List options = getList(new Query().setFilter(new PropertyFilter(Option.OPTION_CATEGORY, FilterOperator.EQUAL, category))); + if (0 == options.size()) { + return null; + } + + options.stream().forEach(option -> ret.put(option.optString(Keys.OBJECT_ID), option.opt(Option.OPTION_VALUE))); + optionCache.putCategory(category, ret); + + return ret; + } catch (final Exception e) { + throw new RepositoryException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/repository/PageRepository.java b/src/main/java/org/b3log/solo/repository/PageRepository.java new file mode 100644 index 00000000..af03dda2 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/PageRepository.java @@ -0,0 +1,198 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.cache.PageCache; +import org.b3log.solo.model.Page; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * Page repository. + * + * @author Liang Ding + * @version 1.0.0.9, Jun 6, 2019 + * @since 0.3.1 + */ +@Repository +public class PageRepository extends AbstractRepository { + + /** + * Page cache. + */ + @Inject + private PageCache pageCache; + + /** + * Public constructor. + */ + public PageRepository() { + super(Page.PAGE); + } + + @Override + public void remove(final String id) throws RepositoryException { + super.remove(id); + + pageCache.removePage(id); + } + + @Override + public JSONObject get(final String id) throws RepositoryException { + JSONObject ret = pageCache.getPage(id); + if (null != ret) { + return ret; + } + + ret = super.get(id); + if (null == ret) { + return null; + } + + pageCache.putPage(ret); + + return ret; + } + + @Override + public void update(final String id, final JSONObject page, final String... propertyNames) throws RepositoryException { + super.update(id, page, propertyNames); + + page.put(Keys.OBJECT_ID, id); + pageCache.putPage(page); + } + + /** + * Gets a page by the specified permalink. + * + * @param permalink the specified permalink + * @return page, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByPermalink(final String permalink) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Page.PAGE_PERMALINK, FilterOperator.EQUAL, permalink)).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets the maximum order. + * + * @return order number, returns {@code -1} if not found + * @throws RepositoryException repository exception + */ + public int getMaxOrder() throws RepositoryException { + final Query query = new Query().addSort(Page.PAGE_ORDER, SortDirection.DESCENDING).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return -1; + } + + return array.optJSONObject(0).optInt(Page.PAGE_ORDER); + } + + /** + * Gets the upper page of the page specified by the given id. + * + * @param id the given id + * @return upper page, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getUpper(final String id) throws RepositoryException { + final JSONObject page = get(id); + if (null == page) { + return null; + } + + final Query query = new Query().setFilter(new PropertyFilter(Page.PAGE_ORDER, FilterOperator.LESS_THAN, page.optInt(Page.PAGE_ORDER))). + addSort(Page.PAGE_ORDER, SortDirection.DESCENDING).setPage(1, 1).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets the under page of the page specified by the given id. + * + * @param id the given id + * @return under page, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getUnder(final String id) throws RepositoryException { + final JSONObject page = get(id); + if (null == page) { + return null; + } + + final Query query = new Query().setFilter(new PropertyFilter(Page.PAGE_ORDER, FilterOperator.GREATER_THAN, page.optInt(Page.PAGE_ORDER))). + addSort(Page.PAGE_ORDER, SortDirection.ASCENDING).setPage(1, 1).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (1 != array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets a page by the specified order. + * + * @param order the specified order + * @return page, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByOrder(final int order) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Page.PAGE_ORDER, FilterOperator.EQUAL, order)).setPageCount(1); + final JSONObject result = get(query); + final JSONArray array = result.optJSONArray(Keys.RESULTS); + if (0 == array.length()) { + return null; + } + + return array.optJSONObject(0); + } + + /** + * Gets pages. + * + * @return a list of pages, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getPages() throws RepositoryException { + final Query query = new Query().addSort(Page.PAGE_ORDER, SortDirection.ASCENDING).setPageCount(1); + + return getList(query); + } +} diff --git a/src/main/java/org/b3log/solo/repository/PluginRepository.java b/src/main/java/org/b3log/solo/repository/PluginRepository.java new file mode 100644 index 00000000..7e1d56d8 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/PluginRepository.java @@ -0,0 +1,40 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.model.Plugin; +import org.b3log.latke.repository.AbstractRepository; +import org.b3log.latke.repository.annotation.Repository; + +/** + * Plugin repository. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 30, 2018 + * @since 0.3.1 + */ +@Repository +public class PluginRepository extends AbstractRepository { + + /** + * Public constructor. + */ + public PluginRepository() { + super(Plugin.PLUGIN); + } +} diff --git a/src/main/java/org/b3log/solo/repository/TagArticleRepository.java b/src/main/java/org/b3log/solo/repository/TagArticleRepository.java new file mode 100644 index 00000000..c0e6c563 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/TagArticleRepository.java @@ -0,0 +1,185 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Tag; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tag-Article repository. + * + * @author Liang Ding + * @version 1.1.1.0, Aug 20, 2019 + * @since 0.3.1 + */ +@Repository +public class TagArticleRepository extends AbstractRepository { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(TagArticleRepository.class); + + /** + * Public constructor. + */ + public TagArticleRepository() { + super(Tag.TAG + "_" + Article.ARTICLE); + } + + /** + * Gets most used tags with the specified number. + * + * @param num the specified number + * @return a list of most used tags, returns an empty list if not found + * @throws RepositoryException repository exception + */ + public List getMostUsedTags(final int num) throws RepositoryException { + final List records = select("SELECT\n" + + "\t`tag_oId`,\n" + + "\tcount(*) AS cnt\n" + + "FROM `" + getName() + "`\n" + + "GROUP BY\n" + + "\t`tag_oId`\n" + + "ORDER BY\n" + + "\tcnt DESC\n" + + "LIMIT ?", num); + final List ret = new ArrayList<>(); + final TagRepository tagRepository = BeanManager.getInstance().getReference(TagRepository.class); + for (final JSONObject record : records) { + final String tagId = record.optString(Tag.TAG + "_" + Keys.OBJECT_ID); + final JSONObject tag = tagRepository.get(tagId); + if (null != tag) { + final int articleCount = getPublishedArticleCount(tagId); + tag.put(Tag.TAG_T_PUBLISHED_REFERENCE_COUNT, articleCount); + } + ret.add(tag); + } + + return ret; + } + + /** + * Gets article count of a tag specified by the given tag id. + * + * @param tagId the given tag id + * @return article count, returns {@code -1} if occurred an exception + */ + public int getArticleCount(final String tagId) { + final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)); + try { + return (int) count(query); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets tag [" + tagId + "]'s article count failed", e); + + return -1; + } + } + + /** + * Gets published article count of a tag specified by the given tag id. + * + * @param tagId the given tag id + * @return published article count, returns {@code -1} if occurred an exception + */ + public int getPublishedArticleCount(final String tagId) { + try { + final String tableNamePrefix = StringUtils.isNotBlank(Latkes.getLocalProperty("jdbc.tablePrefix")) + ? Latkes.getLocalProperty("jdbc.tablePrefix") + "_" + : ""; + final List result = select("SELECT\n" + + "\tcount(*) AS `C`\n" + + "FROM\n" + + "\t" + tableNamePrefix + "tag_article AS t,\n" + + "\t" + tableNamePrefix + "article AS a\n" + + "WHERE\n" + + "\tt.article_oId = a.oId\n" + + "AND a.articleStatus = ?\n" + + "AND t.tag_oId = ?", Article.ARTICLE_STATUS_C_PUBLISHED, tagId); + return result.get(0).optInt("C"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets tag [" + tagId + "]'s published article count failed", e); + + return -1; + } + } + + /** + * Gets tag-article relations by the specified article id. + * + * @param articleId the specified article id + * @return for example + *
+     * [{
+     *         "oId": "",
+     *         "tag_oId": "",
+     *         "article_oId": articleId
+     * }, ....], returns an empty list if not found
+     * 
+ * @throws RepositoryException repository exception + */ + public List getByArticleId(final String articleId) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Article.ARTICLE + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, articleId)). + setPageCount(1); + + return getList(query); + } + + /** + * Gets tag-article relations by the specified tag id. + * + * @param tagId the specified tag id + * @param currentPageNum the specified current page number, MUST greater + * then {@code 0} + * @param pageSize the specified page size(count of a page contains objects), + * MUST greater then {@code 0} + * @return for example + *
+     * {
+     *     "pagination": {
+     *       "paginationPageCount": 88250
+     *     },
+     *     "rslts": [{
+     *         "oId": "",
+     *         "tag_oId": tagId,
+     *         "article_oId": ""
+     *     }, ....]
+     * }
+     * 
+ * @throws RepositoryException repository exception + */ + public JSONObject getByTagId(final String tagId, final int currentPageNum, final int pageSize) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)). + addSort(Article.ARTICLE + "_" + Keys.OBJECT_ID, SortDirection.DESCENDING). + setPage(currentPageNum, pageSize); + + return get(query); + } +} diff --git a/src/main/java/org/b3log/solo/repository/TagRepository.java b/src/main/java/org/b3log/solo/repository/TagRepository.java new file mode 100644 index 00000000..8854d8f6 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/TagRepository.java @@ -0,0 +1,96 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.model.Tag; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tag repository. + * + * @author Liang Ding + * @version 1.0.0.5, Jun 20, 2019 + * @since 0.3.1 + */ +@Repository +public class TagRepository extends AbstractRepository { + + /** + * Public constructor. + */ + public TagRepository() { + super(Tag.TAG); + } + + /** + * Tag-Article relation repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Gets tags of an article specified by the article id. + * + * @param articleId the specified article id + * @return a list of tags of the specified article, returns an empty list + * if not found + * @throws RepositoryException repository exception + */ + public List getByArticleId(final String articleId) throws RepositoryException { + final List ret = new ArrayList<>(); + + final List tagArticleRelations = tagArticleRepository.getByArticleId(articleId); + for (final JSONObject tagArticleRelation : tagArticleRelations) { + final String tagId = tagArticleRelation.optString(Tag.TAG + "_" + Keys.OBJECT_ID); + final JSONObject tag = get(tagId); + + ret.add(tag); + } + + return ret; + } + + /** + * Gets a tag by the specified tag title. + * + * @param tagTitle the specified tag title + * @return a tag, {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByTitle(final String tagTitle) throws RepositoryException { + final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG_TITLE, FilterOperator.EQUAL, tagTitle)).setPageCount(1); + + final JSONObject ret = getFirst(query); + if (null == ret) { + return null; + } + + final String tagId = ret.optString(Keys.OBJECT_ID); + final int articleCount = tagArticleRepository.getPublishedArticleCount(tagId); + ret.put(Tag.TAG_T_PUBLISHED_REFERENCE_COUNT, articleCount); + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/repository/UserRepository.java b/src/main/java/org/b3log/solo/repository/UserRepository.java new file mode 100644 index 00000000..8836ea97 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/UserRepository.java @@ -0,0 +1,120 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Repository; +import org.b3log.solo.cache.UserCache; +import org.json.JSONObject; + +/** + * User repository. + * + * @author Liang Ding + * @version 1.1.0.3, Jun 6, 2019 + * @since 0.3.1 + */ +@Repository +public class UserRepository extends AbstractRepository { + + /** + * User cache. + */ + @Inject + private UserCache userCache; + + /** + * Public constructor. + */ + public UserRepository() { + super(User.USER); + } + + @Override + public void remove(final String id) throws RepositoryException { + super.remove(id); + + userCache.removeUser(id); + } + + @Override + public JSONObject get(final String id) throws RepositoryException { + JSONObject ret = userCache.getUser(id); + if (null != ret) { + return ret; + } + + ret = super.get(id); + if (null == ret) { + return null; + } + + userCache.putUser(ret); + + return ret; + } + + @Override + public void update(final String id, final JSONObject user, final String... propertyNames) throws RepositoryException { + super.update(id, user, propertyNames); + + user.put(Keys.OBJECT_ID, id); + userCache.putUser(user); + + if (Role.ADMIN_ROLE.equals(user.optString(User.USER_ROLE))) { + userCache.putAdmin(user); + } + } + + /** + * Gets a user by the specified username. + * + * @param userName the specified username + * @return user, returns {@code null} if not found + * @throws RepositoryException repository exception + */ + public JSONObject getByUserName(final String userName) throws RepositoryException { + return getFirst(new Query().setFilter(new PropertyFilter(User.USER_NAME, FilterOperator.EQUAL, userName))); + } + + /** + * Gets the administrator user. + * + * @return administrator user, returns {@code null} if not found or error + * @throws RepositoryException repository exception + */ + public JSONObject getAdmin() throws RepositoryException { + JSONObject ret = userCache.getAdmin(); + if (null != ret) { + return ret; + } + + ret = getFirst(new Query().setFilter(new PropertyFilter(User.USER_ROLE, FilterOperator.EQUAL, Role.ADMIN_ROLE))); + if (null == ret) { + return null; + } + + userCache.putAdmin(ret); + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/repository/package-info.java b/src/main/java/org/b3log/solo/repository/package-info.java new file mode 100644 index 00000000..4284b755 --- /dev/null +++ b/src/main/java/org/b3log/solo/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Data access. + */ +package org.b3log.solo.repository; diff --git a/src/main/java/org/b3log/solo/service/ArchiveDateMgmtService.java b/src/main/java/org/b3log/solo/service/ArchiveDateMgmtService.java new file mode 100644 index 00000000..0b6caa05 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/ArchiveDateMgmtService.java @@ -0,0 +1,86 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.ArchiveDate; +import org.b3log.solo.repository.ArchiveDateArticleRepository; +import org.b3log.solo.repository.ArchiveDateRepository; +import org.json.JSONObject; + +import java.util.List; + +/** + * Archive date query service. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 20, 2019 + * @since 3.4.0 + */ +@Service +public class ArchiveDateMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArchiveDateMgmtService.class); + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + + /** + * Removes all unused archive dates. + * + * @return a list of archive dates, returns an empty list if not found + */ + public void removeUnusedArchiveDates() { + final Transaction transaction = archiveDateRepository.beginTransaction(); + try { + final List archiveDates = archiveDateRepository.getArchiveDates(); + for (final JSONObject archiveDate : archiveDates) { + if (1 > archiveDate.optInt(ArchiveDate.ARCHIVE_DATE_T_PUBLISHED_ARTICLE_COUNT)) { + final String archiveDateId = archiveDate.optString(Keys.OBJECT_ID); + archiveDateRepository.remove(archiveDateId); + } + } + transaction.commit(); + } catch (final RepositoryException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Gets archive dates failed", e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/ArchiveDateQueryService.java b/src/main/java/org/b3log/solo/service/ArchiveDateQueryService.java new file mode 100644 index 00000000..b9f1a5df --- /dev/null +++ b/src/main/java/org/b3log/solo/service/ArchiveDateQueryService.java @@ -0,0 +1,121 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.ArchiveDate; +import org.b3log.solo.repository.ArchiveDateArticleRepository; +import org.b3log.solo.repository.ArchiveDateRepository; +import org.json.JSONObject; + +import java.util.List; + +/** + * Archive date query service. + * + * @author Liang Ding + * @version 1.1.0.1, Sep 11, 2019 + * @since 0.4.0 + */ +@Service +public class ArchiveDateQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArchiveDateQueryService.class); + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + /** + * Gets published article count of an archive date specified by the given archive date id. + * + * @param archiveDateId the given archive date id + * @return published article count, returns {@code -1} if occurred an exception + */ + public int getArchiveDatePublishedArticleCount(final String archiveDateId) { + return archiveDateArticleRepository.getPublishedArticleCount(archiveDateId); + } + + /** + * Gets all archive dates. + * + * @return a list of archive dates, returns an empty list if not found + * @throws ServiceException service exception + */ + public List getArchiveDates() throws ServiceException { + try { + return archiveDateRepository.getArchiveDates(); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets archive dates failed", e); + throw new ServiceException("Gets archive dates failed"); + } + } + + /** + * Gets an archive date by the specified archive date string. + * + * @param archiveDateString the specified archive date string (yyyy/MM) + * @return for example, + *
+     * {
+     *     "archiveDate": {
+     *         "oId": "",
+     *         "archiveTime": "",
+     *         "archiveDatePublishedArticleCount": int
+     *     }
+     * }
+     * 
, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getByArchiveDateString(final String archiveDateString) throws ServiceException { + final JSONObject ret = new JSONObject(); + + try { + final JSONObject archiveDate = archiveDateRepository.getByArchiveDate(archiveDateString); + if (null == archiveDate) { + return null; + } + + final int articleCount = archiveDateArticleRepository.getPublishedArticleCount(archiveDate.optString(Keys.OBJECT_ID)); + archiveDate.put(ArchiveDate.ARCHIVE_DATE_T_PUBLISHED_ARTICLE_COUNT, articleCount); + ret.put(ArchiveDate.ARCHIVE_DATE, archiveDate); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets archive date[string=" + archiveDateString + "] failed", e); + throw new ServiceException("Gets archive date[string=" + archiveDateString + "] failed"); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/ArticleMgmtService.java b/src/main/java/org/b3log/solo/service/ArticleMgmtService.java new file mode 100644 index 00000000..3cf71354 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/ArticleMgmtService.java @@ -0,0 +1,1048 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.apache.commons.lang.time.DateUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.event.Event; +import org.b3log.latke.event.EventManager; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Ids; +import org.b3log.solo.event.B3ArticleSender; +import org.b3log.solo.event.EventTypes; +import org.b3log.solo.model.*; +import org.b3log.solo.repository.*; +import org.b3log.solo.util.GitHubs; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.b3log.solo.model.Article.*; + +/** + * Article management service. + * + * @author Liang Ding + * @version 1.3.2.1, Sep 11, 2019 + * @since 0.3.5 + */ +@Service +public class ArticleMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleMgmtService.class); + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Category-tag repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * Permalink query service. + */ + @Inject + private PermalinkQueryService permalinkQueryService; + + /** + * Event manager. + */ + @Inject + private EventManager eventManager; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Statistic query service. + */ + @Inject + private StatisticQueryService statisticQueryService; + + /** + * Init service. + */ + @Inject + private InitService initService; + + /** + * Tag management service. + */ + @Inject + private TagMgmtService tagMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Option management service. + */ + @Inject + private OptionMgmtService optionMgmtService; + + /** + * Refreshes GitHub repos. 同步拉取 GitHub 仓库 https://github.com/b3log/solo/issues/12514 + */ + public void refreshGitHub() { + if (!initService.isInited()) { + return; + } + + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + return; + } + + if (!preference.optBoolean(Option.ID_C_PULL_GITHUB)) { + return; + } + + JSONObject admin; + try { + admin = userRepository.getAdmin(); + } catch (final Exception e) { + return; + } + + if (null == admin) { + return; + } + + final String githubId = admin.optString(UserExt.USER_GITHUB_ID); + final JSONArray gitHubRepos = GitHubs.getGitHubRepos(githubId); + if (null == gitHubRepos || gitHubRepos.isEmpty()) { + return; + } + + JSONObject githubReposOpt = optionQueryService.getOptionById(Option.ID_C_GITHUB_REPOS); + if (null == githubReposOpt) { + githubReposOpt = new JSONObject(); + githubReposOpt.put(Keys.OBJECT_ID, Option.ID_C_GITHUB_REPOS); + githubReposOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_GITHUB); + } + githubReposOpt.put(Option.OPTION_VALUE, gitHubRepos.toString()); + + try { + optionMgmtService.addOrUpdateOption(githubReposOpt); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Updates github repos option failed", e); + + return; + } + + final StringBuilder contentBuilder = new StringBuilder(); + contentBuilder.append("\n"); + contentBuilder.append("\n\n"); + for (int i = 0; i < gitHubRepos.length(); i++) { + final JSONObject repo = gitHubRepos.optJSONObject(i); + final String url = repo.optString("githubrepoHTMLURL"); + final String desc = repo.optString("githubrepoDescription"); + final String name = repo.optString("githubrepoName"); + final String stars = repo.optString("githubrepoStargazersCount"); + final String watchers = repo.optString("githubrepoWatchersCount"); + final String forks = repo.optString("githubrepoForksCount"); + final String lang = repo.optString("githubrepoLanguage"); + final String hp = repo.optString("githubrepoHomepage"); + + String stat = "[🤩`{watchers}`]({url}/watchers \"关注数\")  [⭐️`{stars}`]({url}/stargazers \"收藏数\")  [🖖`{forks}`]({url}/network/members \"分叉数\")"; + stat = stat.replace("{watchers}", watchers).replace("{stars}", stars).replace("{url}", url).replace("{forks}", forks); + if (StringUtils.isNotBlank(hp)) { + stat += "  [\uD83C\uDFE0`{hp}`]({hp} \"项目主页\")"; + stat = stat.replace("{hp}", hp); + } + stat += ""; + contentBuilder.append("### " + (i + 1) + ". [" + name + "](" + url + ") " + lang + " " + stat + "\n\n" + desc + "\n\n"); + if (i < gitHubRepos.length() - 1) { + contentBuilder.append("\n\n---\n\n"); + } + } + final String content = contentBuilder.toString(); + + try { + final String permalink = "/my-github-repos"; + JSONObject article = articleRepository.getByPermalink(permalink); + if (null == article) { + article = new JSONObject(); + article.put(Article.ARTICLE_AUTHOR_ID, admin.optString(Keys.OBJECT_ID)); + article.put(Article.ARTICLE_TITLE, "我在 GitHub 上的开源项目"); + article.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(content)); + article.put(Article.ARTICLE_COMMENT_COUNT, 0); + article.put(Article.ARTICLE_TAGS_REF, "开源,GitHub"); + article.put(Article.ARTICLE_PERMALINK, permalink); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_CONTENT, content); + article.put(Article.ARTICLE_VIEW_PWD, ""); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, false); + + final JSONObject addArticleReq = new JSONObject(); + addArticleReq.put(Article.ARTICLE, article); + addArticle(addArticleReq); + } else { + article.put(Article.ARTICLE_CONTENT, content); + + final String articleId = article.optString(Keys.OBJECT_ID); + final Transaction transaction = articleRepository.beginTransaction(); + articleRepository.update(articleId, article); + transaction.commit(); + } + + final Transaction transaction = pageRepository.beginTransaction(); + JSONObject page = pageRepository.getByPermalink(permalink); + if (null == page) { + page = new JSONObject(); + final int maxOrder = pageRepository.getMaxOrder(); + page.put(Page.PAGE_ORDER, maxOrder + 1); + page.put(Page.PAGE_TITLE, "我的开源"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + page.put(Page.PAGE_PERMALINK, permalink); + page.put(Page.PAGE_ICON, "/images/github-icon.png"); + pageRepository.add(page); + } else { + page.put(Page.PAGE_OPEN_TARGET, "_self"); + page.put(Page.PAGE_ICON, "/images/github-icon.png"); + pageRepository.update(page.optString(Keys.OBJECT_ID), page); + } + transaction.commit(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Updates github repos page failed", e); + } + } + + /** + * Pushes an article specified by the given article id to community. + * + * @param articleId the given article id + */ + public void pushArticleToCommunity(final String articleId) { + try { + final JSONObject article = articleRepository.get(articleId); + if (null == article) { + return; + } + + article.put(Common.POST_TO_COMMUNITY, true); + + final JSONObject data = new JSONObject().put(ARTICLE, article); + B3ArticleSender.pushArticleToRhy(data); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Pushes an article [id=" + articleId + "] to community failed", e); + } + } + + /** + * Article comment count +1 for an article specified by the given article id. + * + * @param articleId the given article id + * @throws JSONException json exception + * @throws RepositoryException repository exception + */ + public void incArticleCommentCount(final String articleId) throws JSONException, RepositoryException { + final JSONObject article = articleRepository.get(articleId); + final JSONObject newArticle = new JSONObject(article, JSONObject.getNames(article)); + final int commentCnt = article.getInt(Article.ARTICLE_COMMENT_COUNT); + newArticle.put(Article.ARTICLE_COMMENT_COUNT, commentCnt + 1); + articleRepository.update(articleId, newArticle, ARTICLE_COMMENT_COUNT); + } + + /** + * Cancels publish an article by the specified article id. + * + * @param articleId the specified article id + * @throws ServiceException service exception + */ + public void cancelPublishArticle(final String articleId) throws ServiceException { + final Transaction transaction = articleRepository.beginTransaction(); + + try { + final JSONObject article = articleRepository.get(articleId); + article.put(ARTICLE_STATUS, ARTICLE_STATUS_C_DRAFT); + articleRepository.update(articleId, article, ARTICLE_STATUS); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Cancels publish article failed", e); + + throw new ServiceException(e); + } + } + + /** + * Puts an article specified by the given article id to top or cancel top. + * + * @param articleId the given article id + * @param top the specified flag, {@code true} to top, {@code false} to + * cancel top + * @throws ServiceException service exception + */ + public void topArticle(final String articleId, final boolean top) throws ServiceException { + final Transaction transaction = articleRepository.beginTransaction(); + + try { + final JSONObject topArticle = articleRepository.get(articleId); + topArticle.put(ARTICLE_PUT_TOP, top); + articleRepository.update(articleId, topArticle, ARTICLE_PUT_TOP); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Can't put the article[oId{0}] to top", articleId); + throw new ServiceException(e); + } + } + + /** + * Updates an article by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "article": { + * "oId": "", + * "articleTitle": "", + * "articleAbstract": "", + * "articleContent": "", + * "articleTags": "tag1,tag2,tag3", // optional, default set "待分类" + * "articlePermalink": "", // optional + * "articleStatus": int, // 0: published, 1: draft + * "articleSignId": "", // optional + * "articleCommentable": boolean, + * "articleViewPwd": "" + * } + * } + * @throws ServiceException service exception + */ + public void updateArticle(final JSONObject requestJSONObject) throws ServiceException { + final Transaction transaction = articleRepository.beginTransaction(); + + try { + final JSONObject article = requestJSONObject.getJSONObject(ARTICLE); + String tagsString = article.optString(Article.ARTICLE_TAGS_REF); + tagsString = Tag.formatTags(tagsString, 4); + if (StringUtils.isBlank(tagsString)) { + tagsString = "待分类"; + } + article.put(Article.ARTICLE_TAGS_REF, tagsString); + + final String articleId = article.getString(Keys.OBJECT_ID); + // Set permalink + final JSONObject oldArticle = articleRepository.get(articleId); + final String permalink = getPermalinkForUpdateArticle(oldArticle, article, oldArticle.optLong(ARTICLE_CREATED)); + article.put(ARTICLE_PERMALINK, permalink); + + processTagsForArticleUpdate(oldArticle, article); + + archiveDate(article); + + if (!oldArticle.getString(Article.ARTICLE_PERMALINK).equals(permalink)) { // The permalink has been updated + // Updates related comments' links + processCommentsForArticleUpdate(article); + } + + // Fill auto properties + fillAutoProperties(oldArticle, article); + // Set date + article.put(ARTICLE_UPDATED, oldArticle.getLong(ARTICLE_UPDATED)); + final long now = System.currentTimeMillis(); + + // The article to update has no sign + if (!article.has(Article.ARTICLE_SIGN_ID)) { + article.put(Article.ARTICLE_SIGN_ID, "0"); + } + + article.put(ARTICLE_UPDATED, now); + + final String articleImg1URL = getArticleImg1URL(article); + article.put(ARTICLE_IMG1_URL, articleImg1URL); + + final String articleAbstractText = Article.getAbstractText(article); + article.put(ARTICLE_ABSTRACT_TEXT, articleAbstractText); + + final boolean postToCommunity = article.optBoolean(Common.POST_TO_COMMUNITY); + article.remove(Common.POST_TO_COMMUNITY); + articleRepository.update(articleId, article); + article.put(Common.POST_TO_COMMUNITY, postToCommunity); + + final boolean publishNewArticle = Article.ARTICLE_STATUS_C_DRAFT == oldArticle.optInt(ARTICLE_STATUS) && Article.ARTICLE_STATUS_C_PUBLISHED == article.optInt(ARTICLE_STATUS); + final JSONObject eventData = new JSONObject(); + eventData.put(ARTICLE, article); + if (publishNewArticle) { + eventManager.fireEventAsynchronously(new Event<>(EventTypes.ADD_ARTICLE, eventData)); + } else { + eventManager.fireEventAsynchronously(new Event<>(EventTypes.UPDATE_ARTICLE, eventData)); + } + + transaction.commit(); + } catch (final ServiceException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates an article failed", e); + + throw e; + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates an article failed", e); + + throw new ServiceException(e.getMessage()); + } + } + + /** + * Adds an article from the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "article": { + * "articleAuthorId": "", + * "articleTitle": "", + * "articleAbstract": "", + * "articleContent": "", + * "articleTags": "tag1,tag2,tag3", + * "articleStatus": int, // 0: published, 1: draft + * "articlePermalink": "", // optional + * "postToCommunity": boolean, // optional + * "articleSignId": "" // optional, default is "0", + * "articleCommentable": boolean, + * "articleCommentCount": int, // optional, default is 0 + * "articleViewPwd": "", + * "oId": "" // optional, generate it if not exists this key + * } + * } + * @return generated article id + * @throws ServiceException service exception + */ + public String addArticle(final JSONObject requestJSONObject) throws ServiceException { + final Transaction transaction = articleRepository.beginTransaction(); + + try { + final JSONObject article = requestJSONObject.getJSONObject(Article.ARTICLE); + String ret = article.optString(Keys.OBJECT_ID); + if (StringUtils.isBlank(ret)) { + ret = Ids.genTimeMillisId(); + article.put(Keys.OBJECT_ID, ret); + } + + String tagsString = article.optString(Article.ARTICLE_TAGS_REF); + tagsString = Tag.formatTags(tagsString, 4); + if (StringUtils.isBlank(tagsString)) { + tagsString = "待分类"; + } + article.put(Article.ARTICLE_TAGS_REF, tagsString); + final String[] tagTitles = tagsString.split(","); + final JSONArray tags = tag(tagTitles, article); + + article.put(Article.ARTICLE_COMMENT_COUNT, article.optInt(Article.ARTICLE_COMMENT_COUNT)); + article.put(Article.ARTICLE_VIEW_COUNT, 0); + if (!article.has(Article.ARTICLE_CREATED)) { + article.put(Article.ARTICLE_CREATED, System.currentTimeMillis()); + } + article.put(Article.ARTICLE_UPDATED, article.optLong(Article.ARTICLE_CREATED)); + article.put(Article.ARTICLE_PUT_TOP, false); + + addTagArticleRelation(tags, article); + + archiveDate(article); + + final String permalink = getPermalinkForAddArticle(article); + article.put(Article.ARTICLE_PERMALINK, permalink); + + final String signId = article.optString(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_SIGN_ID, signId); + + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + + final String articleImg1URL = getArticleImg1URL(article); + article.put(ARTICLE_IMG1_URL, articleImg1URL); + + final String articleAbstractText = Article.getAbstractText(article); + article.put(ARTICLE_ABSTRACT_TEXT, articleAbstractText); + + final boolean postToCommunity = article.optBoolean(Common.POST_TO_COMMUNITY); + article.remove(Common.POST_TO_COMMUNITY); + articleRepository.add(article); + transaction.commit(); + + article.put(Common.POST_TO_COMMUNITY, postToCommunity); + if (Article.ARTICLE_STATUS_C_PUBLISHED == article.optInt(ARTICLE_STATUS)) { + final JSONObject eventData = new JSONObject(); + eventData.put(Article.ARTICLE, article); + eventManager.fireEventAsynchronously(new Event<>(EventTypes.ADD_ARTICLE, eventData)); + } + + return ret; + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e.getMessage()); + } + } + + /** + * Removes the article specified by the given id. + * + * @param articleId the given id + * @throws ServiceException service exception + */ + public void removeArticle(final String articleId) throws ServiceException { + final Transaction transaction = articleRepository.beginTransaction(); + try { + unArchiveDate(articleId); + removeTagArticleRelations(articleId); + articleRepository.remove(articleId); + commentRepository.removeComments(articleId); + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Removes an article[id=" + articleId + "] failed", e); + throw new ServiceException(e); + } + } + + /** + * Updates the random values of articles fetched with the specified update + * count. + * + * @param updateCnt the specified update count + * @throws ServiceException service exception + */ + public void updateArticlesRandomValue(final int updateCnt) throws ServiceException { + final Transaction transaction = articleRepository.beginTransaction(); + + try { + final List randomArticles = articleRepository.getRandomly(updateCnt); + + for (final JSONObject article : randomArticles) { + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + + articleRepository.update(article.getString(Keys.OBJECT_ID), article, ARTICLE_RANDOM_DOUBLE); + } + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.WARN, "Updates article random value failed"); + + throw new ServiceException(e); + } + } + + /** + * Increments the view count of the article specified by the given article id. + * + * @param articleId the given article id + * @throws ServiceException service exception + */ + public void incViewCount(final String articleId) throws ServiceException { + JSONObject article; + + try { + article = articleRepository.get(articleId); + + if (null == article) { + return; + } + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets article [id=" + articleId + "] failed", e); + + return; + } + + final Transaction transaction = articleRepository.beginTransaction(); + + try { + article.put(Article.ARTICLE_VIEW_COUNT, article.getInt(Article.ARTICLE_VIEW_COUNT) + 1); + articleRepository.update(articleId, article, ARTICLE_VIEW_COUNT); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.WARN, "Updates article view count failed"); + + throw new ServiceException(e); + } + } + + /** + * Un-archive an article specified by the given specified article id. + * + * @param articleId the given article id + * @throws ServiceException service exception + */ + private void unArchiveDate(final String articleId) throws ServiceException { + try { + final JSONObject archiveDateArticleRelation = archiveDateArticleRepository.getByArticleId(articleId); + final String archiveDateId = archiveDateArticleRelation.getString(ArchiveDate.ARCHIVE_DATE + "_" + Keys.OBJECT_ID); + final int publishedArticleCount = archiveDateArticleRepository.getPublishedArticleCount(archiveDateId); + if (1 > publishedArticleCount) { + archiveDateRepository.remove(archiveDateId); + } + + archiveDateArticleRepository.remove(archiveDateArticleRelation.getString(Keys.OBJECT_ID)); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Unarchive date for article[id=" + articleId + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Processes comments for article update. + * + * @param article the specified article to update + * @throws Exception exception + */ + private void processCommentsForArticleUpdate(final JSONObject article) throws Exception { + final String articleId = article.getString(Keys.OBJECT_ID); + + final List comments = commentRepository.getComments(articleId, 1, Integer.MAX_VALUE); + for (final JSONObject comment : comments) { + final String commentId = comment.getString(Keys.OBJECT_ID); + final String sharpURL = Comment.getCommentSharpURLForArticle(article, commentId); + comment.put(Comment.COMMENT_SHARP_URL, sharpURL); + if (StringUtils.isBlank(comment.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID))) { + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, ""); + } + if (StringUtils.isBlank(comment.optString(Comment.COMMENT_ORIGINAL_COMMENT_NAME))) { + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, ""); + } + + commentRepository.update(commentId, comment); + } + } + + /** + * Processes tags for article update. + *

+ *

    + *
  • Un-tags old article, decrements tag reference count
  • + *
  • Removes old article-tag relations
  • + *
  • Saves new article-tag relations with tag reference count
  • + *
+ * + * @param oldArticle the specified old article + * @param newArticle the specified new article + * @throws Exception exception + */ + private void processTagsForArticleUpdate(final JSONObject oldArticle, final JSONObject newArticle) throws Exception { + final String oldArticleId = oldArticle.getString(Keys.OBJECT_ID); + final List oldTags = tagRepository.getByArticleId(oldArticleId); + final String tagsString = newArticle.getString(Article.ARTICLE_TAGS_REF); + String[] tagStrings = tagsString.split(","); + final List newTags = new ArrayList<>(); + + for (int i = 0; i < tagStrings.length; i++) { + final String tagTitle = tagStrings[i].trim(); + JSONObject newTag = tagRepository.getByTitle(tagTitle); + + if (null == newTag) { + newTag = new JSONObject(); + newTag.put(Tag.TAG_TITLE, tagTitle); + } + newTags.add(newTag); + } + + final List tagsDropped = new ArrayList<>(); + final List tagsNeedToAdd = new ArrayList<>(); + final List tagsUnchanged = new ArrayList<>(); + + for (final JSONObject newTag : newTags) { + final String newTagTitle = newTag.getString(Tag.TAG_TITLE); + + if (!tagExists(newTagTitle, oldTags)) { + LOGGER.log(Level.DEBUG, "Tag need to add[title={0}]", newTagTitle); + tagsNeedToAdd.add(newTag); + } else { + tagsUnchanged.add(newTag); + } + } + for (final JSONObject oldTag : oldTags) { + final String oldTagTitle = oldTag.getString(Tag.TAG_TITLE); + + if (!tagExists(oldTagTitle, newTags)) { + LOGGER.log(Level.DEBUG, "Tag dropped[title={0}]", oldTag); + tagsDropped.add(oldTag); + } else { + tagsUnchanged.remove(oldTag); + } + } + + LOGGER.log(Level.DEBUG, "Tags unchanged [{0}]", tagsUnchanged); + + final String[] tagIdsDropped = new String[tagsDropped.size()]; + for (int i = 0; i < tagIdsDropped.length; i++) { + final JSONObject tag = tagsDropped.get(i); + final String id = tag.getString(Keys.OBJECT_ID); + tagIdsDropped[i] = id; + } + + removeTagArticleRelations(oldArticleId, 0 == tagIdsDropped.length ? new String[]{"l0y0l"} : tagIdsDropped); + + tagStrings = new String[tagsNeedToAdd.size()]; + for (int i = 0; i < tagStrings.length; i++) { + final JSONObject tag = tagsNeedToAdd.get(i); + final String tagTitle = tag.getString(Tag.TAG_TITLE); + tagStrings[i] = tagTitle; + } + final JSONArray tags = tag(tagStrings, newArticle); + + addTagArticleRelation(tags, newArticle); + } + + /** + * Removes tag-article relations by the specified article id and tag ids of the relations to be removed. + *

+ * Removes all relations if not specified the tag ids. + *

+ * + * @param articleId the specified article id + * @param tagIds the specified tag ids of the relations to be removed + * @throws JSONException json exception + * @throws RepositoryException repository exception + */ + private void removeTagArticleRelations(final String articleId, final String... tagIds) throws JSONException, RepositoryException { + final List tagIdList = Arrays.asList(tagIds); + final List tagArticleRelations = tagArticleRepository.getByArticleId(articleId); + + for (int i = 0; i < tagArticleRelations.size(); i++) { + final JSONObject tagArticleRelation = tagArticleRelations.get(i); + String relationId; + if (tagIdList.isEmpty()) { // Removes all if un-specified + relationId = tagArticleRelation.getString(Keys.OBJECT_ID); + tagArticleRepository.remove(relationId); + } else { + if (tagIdList.contains(tagArticleRelation.getString(Tag.TAG + "_" + Keys.OBJECT_ID))) { + relationId = tagArticleRelation.getString(Keys.OBJECT_ID); + tagArticleRepository.remove(relationId); + } + } + + final String tagId = tagArticleRelation.optString(Tag.TAG + "_" + Keys.OBJECT_ID); + final int articleCount = tagArticleRepository.getArticleCount(tagId); + if (1 > articleCount) { + categoryTagRepository.removeByTagId(tagId); + tagRepository.remove(tagId); + } + } + } + + /** + * Adds relation of the specified tags and article. + * + * @param tags the specified tags + * @param article the specified article + * @throws RepositoryException repository exception + */ + private void addTagArticleRelation(final JSONArray tags, final JSONObject article) throws RepositoryException { + for (int i = 0; i < tags.length(); i++) { + final JSONObject tag = tags.optJSONObject(i); + final JSONObject tagArticleRelation = new JSONObject(); + + tagArticleRelation.put(Tag.TAG + "_" + Keys.OBJECT_ID, tag.optString(Keys.OBJECT_ID)); + tagArticleRelation.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, article.optString(Keys.OBJECT_ID)); + + tagArticleRepository.add(tagArticleRelation); + } + } + + /** + * Tags the specified article with the specified tag titles. + * + * @param tagTitles the specified tag titles + * @param article the specified article + * @return an array of tags + * @throws RepositoryException repository exception + */ + private JSONArray tag(final String[] tagTitles, final JSONObject article) throws RepositoryException { + final JSONArray ret = new JSONArray(); + + for (int i = 0; i < tagTitles.length; i++) { + final String tagTitle = tagTitles[i].trim(); + JSONObject tag = tagRepository.getByTitle(tagTitle); + String tagId; + + if (null == tag) { + LOGGER.log(Level.TRACE, "Found a new tag[title={0}] in article[title={1}]", + tagTitle, article.optString(Article.ARTICLE_TITLE)); + tag = new JSONObject(); + tag.put(Tag.TAG_TITLE, tagTitle); + tagId = tagRepository.add(tag); + tag.put(Keys.OBJECT_ID, tagId); + } else { + tagId = tag.optString(Keys.OBJECT_ID); + LOGGER.log(Level.TRACE, "Found a existing tag[title={0}, id={1}] in article[title={2}]", + tag.optString(Tag.TAG_TITLE), tag.optString(Keys.OBJECT_ID), article.optString(Article.ARTICLE_TITLE)); + final JSONObject tagTmp = new JSONObject(); + tagTmp.put(Keys.OBJECT_ID, tagId); + tagTmp.put(Tag.TAG_TITLE, tagTitle); + tagRepository.update(tagId, tagTmp); + } + + ret.put(tag); + } + + return ret; + } + + /** + * Archive the create date with the specified article. + * + * @param article the specified article, for example, + * { + * "oId": "", + * .... + * } + * @throws RepositoryException repository exception + */ + private void archiveDate(final JSONObject article) throws RepositoryException { + if (Article.ARTICLE_STATUS_C_PUBLISHED != article.optInt(ARTICLE_STATUS)) { + return; + } + + final long created = article.optLong(Keys.OBJECT_ID); + final String createDateString = DateFormatUtils.format(created, "yyyy/MM"); + JSONObject archiveDate = archiveDateRepository.getByArchiveDate(createDateString); + if (null == archiveDate) { + archiveDate = new JSONObject(); + try { + archiveDate.put(ArchiveDate.ARCHIVE_TIME, DateUtils.parseDate(createDateString, new String[]{"yyyy/MM"}).getTime()); + archiveDateRepository.add(archiveDate); + } catch (final ParseException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + throw new RepositoryException(e); + } + } + + final JSONObject archiveDateArticleRelation = new JSONObject(); + archiveDateArticleRelation.put(ArchiveDate.ARCHIVE_DATE + "_" + Keys.OBJECT_ID, archiveDate.optString(Keys.OBJECT_ID)); + archiveDateArticleRelation.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, article.optString(Keys.OBJECT_ID)); + archiveDateArticleRepository.add(archiveDateArticleRelation); + } + + /** + * Fills 'auto' properties for the specified article and old article. + *

+ * Some properties of an article are not been changed while article + * updating, these properties are called 'auto' properties. + *

+ *

+ * The property(named {@value org.b3log.solo.model.Article#ARTICLE_RANDOM_DOUBLE}) of the specified + * article will be regenerated. + *

+ * + * @param oldArticle the specified old article + * @param article the specified article + * @throws JSONException json exception + */ + private void fillAutoProperties(final JSONObject oldArticle, final JSONObject article) throws JSONException { + final long created = oldArticle.getLong(ARTICLE_CREATED); + article.put(ARTICLE_CREATED, created); + article.put(ARTICLE_COMMENT_COUNT, oldArticle.getInt(ARTICLE_COMMENT_COUNT)); + article.put(ARTICLE_VIEW_COUNT, oldArticle.getInt(ARTICLE_VIEW_COUNT)); + article.put(ARTICLE_PUT_TOP, oldArticle.getBoolean(ARTICLE_PUT_TOP)); + article.put(ARTICLE_AUTHOR_ID, oldArticle.getString(ARTICLE_AUTHOR_ID)); + article.put(ARTICLE_RANDOM_DOUBLE, Math.random()); + } + + /** + * Gets article permalink for adding article with the specified article. + * + * @param article the specified article + * @return permalink + * @throws ServiceException if invalid permalink occurs + */ + private String getPermalinkForAddArticle(final JSONObject article) throws ServiceException { + final long date = article.optLong(Article.ARTICLE_CREATED); + String ret = article.optString(Article.ARTICLE_PERMALINK); + if (StringUtils.isBlank(ret)) { + ret = "/articles/" + DateFormatUtils.format(date, "yyyy/MM/dd") + "/" + article.optString(Keys.OBJECT_ID) + ".html"; + } + + if (!ret.startsWith("/")) { + ret = "/" + ret; + } + + if (PermalinkQueryService.invalidArticlePermalinkFormat(ret)) { + throw new ServiceException(langPropsService.get("invalidPermalinkFormatLabel")); + } + + if (permalinkQueryService.exist(ret)) { + throw new ServiceException(langPropsService.get("duplicatedPermalinkLabel")); + } + + return ret.replaceAll(" ", "-"); + } + + /** + * Gets article permalink for updating article with the specified old article, article, created at. + * + * @param oldArticle the specified old article + * @param article the specified article + * @param created the specified created + * @return permalink + * @throws ServiceException if invalid permalink occurs + * @throws JSONException json exception + */ + private String getPermalinkForUpdateArticle(final JSONObject oldArticle, final JSONObject article, final long created) + throws ServiceException, JSONException { + final String articleId = article.getString(Keys.OBJECT_ID); + String ret = article.optString(ARTICLE_PERMALINK).trim(); + final String oldPermalink = oldArticle.getString(ARTICLE_PERMALINK); + + if (!oldPermalink.equals(ret)) { + if (StringUtils.isBlank(ret)) { + ret = "/articles/" + DateFormatUtils.format(created, "yyyy/MM/dd") + "/" + articleId + ".html"; + } + + if (!ret.startsWith("/")) { + ret = "/" + ret; + } + + if (PermalinkQueryService.invalidArticlePermalinkFormat(ret)) { + throw new ServiceException(langPropsService.get("invalidPermalinkFormatLabel")); + } + + if (!oldPermalink.equals(ret) && permalinkQueryService.exist(ret)) { + throw new ServiceException(langPropsService.get("duplicatedPermalinkLabel")); + } + } + + return ret.replaceAll(" ", "-"); + } + + /** + * Determines whether the specified tag title exists in the specified tags. + * + * @param tagTitle the specified tag title + * @param tags the specified tags + * @return {@code true} if it exists, {@code false} otherwise + * @throws JSONException json exception + */ + private static boolean tagExists(final String tagTitle, final List tags) throws JSONException { + for (final JSONObject tag : tags) { + if (tag.getString(Tag.TAG_TITLE).equals(tagTitle)) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/org/b3log/solo/service/ArticleQueryService.java b/src/main/java/org/b3log/solo/service/ArticleQueryService.java new file mode 100644 index 00000000..d78320db --- /dev/null +++ b/src/main/java/org/b3log/solo/service/ArticleQueryService.java @@ -0,0 +1,994 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.*; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.util.CollectionUtils; +import org.b3log.latke.util.Paginator; +import org.b3log.latke.util.Stopwatchs; +import org.b3log.solo.model.*; +import org.b3log.solo.repository.*; +import org.b3log.solo.util.Markdowns; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.*; + +/** + * Article query service. + * + * @author Liang Ding + * @author ArmstrongCN + * @author Zephyr + * @author Liyuan Li + * @version 1.3.3.0, Sep 11, 2019 + * @since 0.3.5 + */ +@Service +public class ArticleQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ArticleQueryService.class); + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Category-Tag repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * User service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + /** + * Statistic query service. + */ + @Inject + private StatisticQueryService statisticQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Searches articles with the specified keyword. + * + * @param keyword the specified keyword + * @param currentPageNum the specified current page number + * @param pageSize the specified page size + * @return result + */ + public JSONObject searchKeyword(final String keyword, final int currentPageNum, final int pageSize) { + final JSONObject ret = new JSONObject(); + ret.put(Article.ARTICLES, (Object) Collections.emptyList()); + + final JSONObject pagination = new JSONObject(); + ret.put(Pagination.PAGINATION, pagination); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, 0); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, (Object) Collections.emptyList()); + + try { + final Query query = new Query().setFilter( + CompositeFilterOperator.and(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED), + CompositeFilterOperator.or( + new PropertyFilter(Article.ARTICLE_TITLE, FilterOperator.LIKE, "%" + keyword + "%"), + new PropertyFilter(Article.ARTICLE_CONTENT, FilterOperator.LIKE, "%" + keyword + "%")))). + addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING).setPage(currentPageNum, pageSize); + + final JSONObject result = articleRepository.get(query); + + final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + final JSONObject preference = optionQueryService.getPreference(); + final int windowSize = preference.optInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, (Object) pageNums); + + final List articles = CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS)); + ret.put(Article.ARTICLES, (Object) articles); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Searches articles error", e); + } + + return ret; + } + + /** + * Gets category articles. + * + * @param categoryId the specified category id + * @param currentPageNum the specified current page number + * @param pageSize the specified page size + * @return result + * @throws ServiceException service exception + */ + public JSONObject getCategoryArticles(final String categoryId, final int currentPageNum, final int pageSize) throws ServiceException { + final JSONObject ret = new JSONObject(); + ret.put(Keys.RESULTS, (Object) Collections.emptyList()); + + final JSONObject pagination = new JSONObject(); + ret.put(Pagination.PAGINATION, pagination); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, 0); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, (Object) Collections.emptyList()); + + try { + final JSONArray categoryTags = categoryTagRepository.getByCategoryId(categoryId, 1, Integer.MAX_VALUE).optJSONArray(Keys.RESULTS); + if (categoryTags.length() <= 0) { + return ret; + } + + final List tagIds = new ArrayList<>(); + for (int i = 0; i < categoryTags.length(); i++) { + tagIds.add(categoryTags.optJSONObject(i).optString(Tag.TAG + "_" + Keys.OBJECT_ID)); + } + + final StringBuilder queryCount = new StringBuilder("SELECT count(DISTINCT(b3_solo_article.oId)) as C FROM "); + final StringBuilder queryList = new StringBuilder("SELECT DISTINCT(b3_solo_article.oId) ").append(" FROM "); + final StringBuilder queryStr = new StringBuilder(articleRepository.getName() + " AS b3_solo_article,"). + append(tagArticleRepository.getName() + " AS b3_solo_tag_article"). + append(" WHERE b3_solo_article.oId=b3_solo_tag_article.article_oId "). + append(" AND b3_solo_article.").append(Article.ARTICLE_STATUS).append("=?"). + append(" AND ").append("b3_solo_tag_article.tag_oId").append(" IN ("); + for (int i = 0; i < tagIds.size(); i++) { + queryStr.append(" ").append(tagIds.get(i)); + if (i < (tagIds.size() - 1)) { + queryStr.append(","); + } + } + queryStr.append(") ORDER BY ").append("b3_solo_tag_article.oId DESC"); + + final List tagArticlesCountResult = articleRepository. + select(queryCount.append(queryStr.toString()).toString(), Article.ARTICLE_STATUS_C_PUBLISHED); + queryStr.append(" LIMIT ").append((currentPageNum - 1) * pageSize).append(",").append(pageSize); + final List tagArticles = articleRepository. + select(queryList.append(queryStr.toString()).toString(), Article.ARTICLE_STATUS_C_PUBLISHED); + if (tagArticles.size() <= 0) { + return ret; + } + + final int tagArticlesCount = tagArticlesCountResult == null ? 0 : tagArticlesCountResult.get(0).optInt("C"); + final int pageCount = (int) Math.ceil(tagArticlesCount / (double) pageSize); + final JSONObject preference = optionQueryService.getPreference(); + final int windowSize = preference.optInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, (Object) pageNums); + pagination.put(Pagination.PAGINATION_RECORD_COUNT, tagArticlesCount); + + final Set articleIds = new HashSet<>(); + for (int i = 0; i < tagArticles.size(); i++) { + articleIds.add(tagArticles.get(i).optString(Keys.OBJECT_ID)); + } + final Query query = new Query().setFilter(new PropertyFilter(Keys.OBJECT_ID, FilterOperator.IN, articleIds)). + setPageCount(1).addSort(Keys.OBJECT_ID, SortDirection.DESCENDING); + final List articles = new ArrayList<>(); + final JSONArray articleArray = articleRepository.get(query).optJSONArray(Keys.RESULTS); + for (int i = 0; i < articleArray.length(); i++) { + final JSONObject article = articleArray.optJSONObject(i); + article.put(Article.ARTICLE_CREATE_TIME, article.optLong(Article.ARTICLE_CREATED)); + article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.optLong(Article.ARTICLE_CREATED))); + article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED))); + articles.add(article); + } + ret.put(Keys.RESULTS, (Object) articles); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets category articles error", e); + + throw new ServiceException(e); + } + } + + /** + * Can the specified user access an article specified by the given article id? + * + * @param articleId the given article id + * @param user the specified user + * @return {@code true} if the current user can access the article, {@code false} otherwise + * @throws ServiceException service exception + */ + public boolean canAccessArticle(final String articleId, final JSONObject user) throws ServiceException { + if (StringUtils.isBlank(articleId)) { + return false; + } + + if (null == user) { + return false; + } + + if (Role.ADMIN_ROLE.equals(user.optString(User.USER_ROLE))) { + return true; + } + + try { + final JSONObject article = articleRepository.get(articleId); + final String currentUserId = user.getString(Keys.OBJECT_ID); + + return article.getString(Article.ARTICLE_AUTHOR_ID).equals(currentUserId); + } catch (final Exception e) { + throw new ServiceException(e); + } + } + + /** + * Gets time of the recent updated article. + * + * @return time of the recent updated article, returns {@code 0} if not found + */ + public long getRecentArticleTime() { + try { + final List recentArticles = articleRepository.getRecentArticles(1); + if (recentArticles.isEmpty()) { + return 0; + } + + final JSONObject recentArticle = recentArticles.get(0); + + return recentArticle.getLong(Article.ARTICLE_UPDATED); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets recent article time failed", e); + + return 0; + } + } + + /** + * Gets the specified article's author. + *

+ * The specified article has a property {@value Article#ARTICLE_AUTHOR_ID}, this method will use this property to + * get a user from users. + *

+ *

+ * If can't find the specified article's author (i.e. the author has been removed by administrator), returns + * administrator. + *

+ * + * @param article the specified article + * @return user, {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getAuthor(final JSONObject article) throws ServiceException { + try { + final String userId = article.getString(Article.ARTICLE_AUTHOR_ID); + JSONObject ret = userRepository.get(userId); + if (null == ret) { + LOGGER.log(Level.WARN, "Gets author of article failed, assumes the administrator is the author of this article [id={0}]", + article.getString(Keys.OBJECT_ID)); + // This author may be deleted by admin, use admin as the author of this article + ret = userRepository.getAdmin(); + } + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets author of article [id={0}] failed", article.optString(Keys.OBJECT_ID)); + + throw new ServiceException(e); + } + } + + /** + * Gets the sign of an article specified by the sign id. + * + * @param signId the specified article id + * @param preference the specified preference + * @return article sign, returns the default sign (which oId is "1") if not found + * @throws JSONException json exception + */ + public JSONObject getSign(final String signId, final JSONObject preference) throws JSONException { + final JSONArray signs = new JSONArray(preference.getString(Option.ID_C_SIGNS)); + JSONObject defaultSign = null; + for (int i = 0; i < signs.length(); i++) { + final JSONObject ret = signs.getJSONObject(i); + + if (signId.equals(ret.optString(Keys.OBJECT_ID))) { + return ret; + } + + if ("1".equals(ret.optString(Keys.OBJECT_ID))) { + defaultSign = ret; + } + } + + LOGGER.log(Level.WARN, "Can not find the sign [id={0}], returns a default sign [id=1]", signId); + + return defaultSign; + } + + /** + * Determines the specified article has updated. + * + * @param article the specified article + * @return {@code true} if it has updated, {@code false} otherwise + * @throws JSONException json exception + */ + public boolean hasUpdated(final JSONObject article) throws JSONException { + final long updateDate = article.getLong(Article.ARTICLE_UPDATED); + final long createDate = article.getLong(Article.ARTICLE_CREATED); + + return createDate != updateDate; + } + + /** + * Gets the recent articles with the specified fetch size. + * + * @param fetchSize the specified fetch size + * @return a list of json object, its size less or equal to the specified fetch size + */ + public List getRecentArticles(final int fetchSize) { + try { + return articleRepository.getRecentArticles(fetchSize); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets recent articles failed", e); + + return Collections.emptyList(); + } + } + + /** + * Gets an article by the specified article id. + *

+ * Note: The article content and abstract is raw (no editor type processing). + *

+ * + * @param articleId the specified article id + * @return for example,
+     * {
+     *     "article": {
+     *         "oId": "",
+     *         "articleTitle": "",
+     *         "articleAbstract": "",
+     *         "articleContent": "",
+     *         "articlePermalink": "",
+     *         "articleCreateDate": java.util.Date,
+     *         "articleTags": [{
+     *             "oId": "",
+     *             "tagTitle": ""
+     *         }, ....],
+     *         "articleSignId": "",
+     *         "articleViewPwd": "",
+     *         "signs": [{
+     *             "oId": "",
+     *             "signHTML": ""
+     *         }, ....]
+     *     }
+     * }
+     * 
, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getArticle(final String articleId) throws ServiceException { + try { + final JSONObject ret = new JSONObject(); + final JSONObject article = articleRepository.get(articleId); + if (null == article) { + return null; + } + + ret.put(Article.ARTICLE, article); + + // Tags + final JSONArray tags = new JSONArray(); + final List tagArticleRelations = tagArticleRepository.getByArticleId(articleId); + for (final JSONObject tagArticleRelation : tagArticleRelations) { + final String tagId = tagArticleRelation.getString(Tag.TAG + "_" + Keys.OBJECT_ID); + final JSONObject tag = tagRepository.get(tagId); + + tags.put(tag); + } + article.put(Article.ARTICLE_TAGS_REF, tags); + + // Signs + final JSONObject preference = optionQueryService.getPreference(); + article.put(Sign.SIGNS, new JSONArray(preference.getString(Option.ID_C_SIGNS))); + // Remove unused properties + article.remove(Article.ARTICLE_AUTHOR_ID); + article.remove(Article.ARTICLE_COMMENT_COUNT); + article.remove(Article.ARTICLE_PUT_TOP); + article.remove(Article.ARTICLE_UPDATED); + article.remove(Article.ARTICLE_VIEW_COUNT); + article.remove(Article.ARTICLE_RANDOM_DOUBLE); + + LOGGER.log(Level.DEBUG, "Got an article [id={0}]", articleId); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets an article failed", e); + throw new ServiceException(e); + } + } + + /** + * Gets articles(by crate date descending) by the specified request json object. + *

+ * Specified the "excludes" for results properties exclusion. + *

+ * + * @param requestJSONObject the specified request json object, for example, + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10, + * "articleStatus": int, + * "keyword": "", // Optional search keyword + * "excludes": ["", ....], // Optional + * "enableArticleUpdateHint": bool // Optional + * see {@link Pagination} for more details + * @return for example,
+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "articles": [{
+     *         "oId": "",
+     *         "articleTitle": "",
+     *         "articleCommentCount": int,
+     *         "articleCreateTime"; long,
+     *         "articleViewCount": int,
+     *         "articleTags": "tag1, tag2, ....",
+     *         "articlePutTop": boolean,
+     *         "articleSignId": "",
+     *         "articleViewPwd": "",
+     *         .... // Specified by the "excludes"
+     *      }, ....]
+     * }
+     * 
, order by article update date and sticky(put top). + * @see Pagination + */ + public JSONObject getArticles(final JSONObject requestJSONObject) { + final JSONObject ret = new JSONObject(); + + try { + final int currentPageNum = requestJSONObject.getInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.getInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.getInt(Pagination.PAGINATION_WINDOW_SIZE); + final int articleStatus = requestJSONObject.optInt(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + + final Query query = new Query().setPage(currentPageNum, pageSize). + addSort(Article.ARTICLE_PUT_TOP, SortDirection.DESCENDING); + if (requestJSONObject.optBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)) { + query.addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING); + } else { + query.addSort(Article.ARTICLE_CREATED, SortDirection.DESCENDING); + } + final String keyword = requestJSONObject.optString(Common.KEYWORD); + if (StringUtils.isBlank(keyword)) { + query.setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, articleStatus)); + } else { + query.setFilter(CompositeFilterOperator.and( + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, articleStatus), + CompositeFilterOperator.or( + new PropertyFilter(Article.ARTICLE_TITLE, FilterOperator.LIKE, "%" + keyword + "%"), + new PropertyFilter(Article.ARTICLE_TAGS_REF, FilterOperator.LIKE, "%" + keyword + "%") + ) + )); + } + + final JSONObject result = articleRepository.get(query); + final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + final JSONObject pagination = new JSONObject(); + ret.put(Pagination.PAGINATION, pagination); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + final JSONArray articles = result.getJSONArray(Keys.RESULTS); + JSONArray excludes = requestJSONObject.optJSONArray(Keys.EXCLUDES); + excludes = null == excludes ? new JSONArray() : excludes; + + for (int i = 0; i < articles.length(); i++) { + final JSONObject article = articles.getJSONObject(i); + final JSONObject author = getAuthor(article); + final String authorName = author.getString(User.USER_NAME); + article.put(Common.AUTHOR_NAME, authorName); + article.put(Article.ARTICLE_CREATE_TIME, article.getLong(Article.ARTICLE_CREATED)); + article.put(Article.ARTICLE_UPDATE_TIME, article.getLong(Article.ARTICLE_UPDATED)); + + // Remove unused properties + for (int j = 0; j < excludes.length(); j++) { + article.remove(excludes.optString(j)); + } + } + + ret.put(Article.ARTICLES, articles); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets articles failed", e); + + return null; + } + } + + /** + * Gets a list of published articles with the specified tag id, current page number and page size. + * + * @param tagId the specified tag id + * @param currentPageNum the specified current page number + * @param pageSize the specified page size + * @return result, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getArticlesByTag(final String tagId, final int currentPageNum, final int pageSize) throws ServiceException { + try { + JSONObject result = tagArticleRepository.getByTagId(tagId, currentPageNum, pageSize); + if (null == result) { + return null; + } + final JSONArray tagArticleRelations = result.getJSONArray(Keys.RESULTS); + if (0 == tagArticleRelations.length()) { + return null; + } + final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION); + + final Set articleIds = new HashSet<>(); + for (int i = 0; i < tagArticleRelations.length(); i++) { + final JSONObject tagArticleRelation = tagArticleRelations.getJSONObject(i); + final String articleId = tagArticleRelation.getString(Article.ARTICLE + "_" + Keys.OBJECT_ID); + articleIds.add(articleId); + } + + final List retArticles = new ArrayList<>(); + + final Query query = new Query().setFilter(CompositeFilterOperator.and( + new PropertyFilter(Keys.OBJECT_ID, FilterOperator.IN, articleIds), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + setPageCount(1). + addSort(Keys.OBJECT_ID, SortDirection.DESCENDING); + final List articles = articleRepository.getList(query); + for (final JSONObject article : articles) { + article.put(Article.ARTICLE_CREATE_TIME, article.getLong(Article.ARTICLE_CREATED)); + article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.getLong(Article.ARTICLE_CREATED))); + article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED))); + retArticles.add(article); + } + final JSONObject ret = new JSONObject(); + ret.put(Pagination.PAGINATION, pagination); + ret.put(Keys.RESULTS, (Object) retArticles); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets articles by tag [id=" + tagId + "] failed", e); + throw new ServiceException(e); + } + } + + /** + * Gets a list of published articles with the specified archive date id, current page number and page size. + * + * @param archiveDateId the specified archive date id + * @param currentPageNum the specified current page number + * @param pageSize the specified page size + * @return a list of articles, returns an empty list if not found + * @throws ServiceException service exception + */ + public List getArticlesByArchiveDate(final String archiveDateId, final int currentPageNum, final int pageSize) throws ServiceException { + try { + JSONObject result = archiveDateArticleRepository.getByArchiveDateId(archiveDateId, currentPageNum, pageSize); + final JSONArray relations = result.getJSONArray(Keys.RESULTS); + if (0 == relations.length()) { + return Collections.emptyList(); + } + + final Set articleIds = new HashSet<>(); + for (int i = 0; i < relations.length(); i++) { + final JSONObject relation = relations.getJSONObject(i); + final String articleId = relation.getString(Article.ARTICLE + "_" + Keys.OBJECT_ID); + + articleIds.add(articleId); + } + + final List ret = new ArrayList<>(); + final Query query = new Query().setFilter(CompositeFilterOperator.and( + new PropertyFilter(Keys.OBJECT_ID, FilterOperator.IN, articleIds), + new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))). + setPageCount(1). + addSort(Keys.OBJECT_ID, SortDirection.DESCENDING); + final List articles = articleRepository.getList(query); + for (final JSONObject article : articles) { + article.put(Article.ARTICLE_CREATE_TIME, article.getLong(Article.ARTICLE_CREATED)); + article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.getLong(Article.ARTICLE_CREATED))); + article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED))); + ret.add(article); + } + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets articles by archive date[id=" + archiveDateId + "] failed", e); + throw new ServiceException(e); + } + } + + /** + * Gets a list of articles randomly with the specified fetch size. + *

+ * Note: The article content and abstract is raw (no editor type processing). + *

+ * + * @param fetchSize the specified fetch size + * @return a list of json objects, its size less or equal to the specified fetch size + * @throws ServiceException service exception + */ + public List getArticlesRandomly(final int fetchSize) throws ServiceException { + try { + final List ret = articleRepository.getRandomly(fetchSize); + removeUnusedProperties(ret); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets articles randomly failed[fetchSize=" + fetchSize + "]", e); + throw new ServiceException(e); + } + } + + /** + * Gets the relevant published articles of the specified article. + *

+ * Note: The article content and abstract is raw (no editor type processing). + *

+ * + * @param article the specified article + * @param preference the specified preference + * @return a list of articles, returns an empty list if not found + */ + public List getRelevantArticles(final JSONObject article, final JSONObject preference) { + try { + final int displayCnt = preference.getInt(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT); + final String[] tagTitles = article.getString(Article.ARTICLE_TAGS_REF).split(","); + final int maxTagCnt = displayCnt > tagTitles.length ? tagTitles.length : displayCnt; + final String articleId = article.getString(Keys.OBJECT_ID); + + final List articles = new ArrayList<>(); + + for (int i = 0; i < maxTagCnt; i++) { // XXX: should average by tag? + final String tagTitle = tagTitles[i]; + final JSONObject tag = tagRepository.getByTitle(tagTitle); + final String tagId = tag.getString(Keys.OBJECT_ID); + final JSONObject result = tagArticleRepository.getByTagId(tagId, 1, displayCnt); + final JSONArray tagArticleRelations = result.getJSONArray(Keys.RESULTS); + + final int relationSize = displayCnt < tagArticleRelations.length() ? displayCnt : tagArticleRelations.length(); + + for (int j = 0; j < relationSize; j++) { + final JSONObject tagArticleRelation = tagArticleRelations.getJSONObject(j); + final String relatedArticleId = tagArticleRelation.getString(Article.ARTICLE + "_" + Keys.OBJECT_ID); + + if (articleId.equals(relatedArticleId)) { + continue; + } + + final JSONObject relevant = articleRepository.get(relatedArticleId); + + if (Article.ARTICLE_STATUS_C_PUBLISHED != relevant.optInt(Article.ARTICLE_STATUS)) { + continue; + } + + boolean existed = false; + + for (final JSONObject relevantArticle : articles) { + if (relevantArticle.getString(Keys.OBJECT_ID).equals(relevant.getString(Keys.OBJECT_ID))) { + existed = true; + } + } + + if (!existed) { + articles.add(relevant); + } + } + } + + removeUnusedProperties(articles); + + if (displayCnt > articles.size()) { + return articles; + } + + final List randomIntegers = CollectionUtils.getRandomIntegers(0, articles.size() - 1, displayCnt); + final List ret = new ArrayList<>(); + for (final int index : randomIntegers) { + ret.add(articles.get(index)); + } + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets relevant articles failed", e); + + return Collections.emptyList(); + } + } + + /** + * Gets the next article(by create date) by the specified article id. + *

+ * Note: The article content and abstract is raw (no editor type processing). + *

+ * + * @param articleId the specified article id + * @return the previous article,
+     * {
+     *     "articleTitle": "",
+     *     "articlePermalink": "",
+     *     "articleAbstract": ""
+     * }
+     * 
returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getNextArticle(final String articleId) throws ServiceException { + try { + return articleRepository.getNextArticle(articleId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets the next article failed[articleId=" + articleId + "]", e); + throw new ServiceException(e); + } + } + + /** + * Gets the previous article(by create date) by the specified article id. + *

+ * Note: The article content and abstract is raw (no editor type processing). + *

+ * + * @param articleId the specified article id + * @return the previous article,
+     * {
+     *     "articleTitle": "",
+     *     "articlePermalink": "",
+     *     "articleAbstract": ""
+     * }
+     * 
returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getPreviousArticle(final String articleId) throws ServiceException { + try { + return articleRepository.getPreviousArticle(articleId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets the previous article failed[articleId=" + articleId + "]", e); + throw new ServiceException(e); + } + } + + /** + * Gets an article by the specified article id. + *

+ * Note: The article content and abstract is raw (no editor type processing). + *

+ * + * @param articleId the specified article id + * @return an article, returns {@code null} if not found + */ + public JSONObject getArticleById(final String articleId) { + try { + return articleRepository.get(articleId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets an article [id=" + articleId + "] failed", e); + + return null; + } + } + + /** + * Gets published articles by the specified author id, current page number and page size. + * + * @param authorId the specified author id + * @param currentPageNum the specified current page number + * @param pageSize the specified page size + * @return result + * @throws ServiceException service exception + */ + public JSONObject getArticlesByAuthorId(final String authorId, final int currentPageNum, final int pageSize) throws ServiceException { + try { + final JSONObject ret = articleRepository.getByAuthorId(authorId, currentPageNum, pageSize); + final JSONArray articles = ret.getJSONArray(Keys.RESULTS); + for (int i = 0; i < articles.length(); i++) { + final JSONObject article = articles.getJSONObject(i); + article.put(Article.ARTICLE_CREATE_TIME, article.getLong(Article.ARTICLE_CREATED)); + article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.optLong(Article.ARTICLE_CREATED))); + article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED))); + } + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets articles by author id failed [authorId=" + authorId + ", currentPageNum=" + currentPageNum + ", pageSize=" + pageSize + "]", e); + + throw new ServiceException(e); + } + } + + /** + * Gets article contents with the specified article id. + *

+ * Invoking this method dose not effect on article view count. + *

+ * + * @param context the specified HTTP servlet request context + * @param articleId the specified article id + * @return article contents, returns {@code null} if not found + * @throws ServiceException service exception + */ + public String getArticleContent(final RequestContext context, final String articleId) throws ServiceException { + if (StringUtils.isBlank(articleId)) { + return null; + } + + try { + final JSONObject article = articleRepository.get(articleId); + if (null == article) { + return null; + } + + if (null != context && Solos.needViewPwd(context, article)) { + final String content = langPropsService.get("articleContentPwd"); + article.put(Article.ARTICLE_CONTENT, content); + } else { + // Markdown to HTML for content and abstract + Stopwatchs.start("Get Article Content [Markdown]"); + String content = article.optString(Article.ARTICLE_CONTENT); + content = Markdowns.toHTML(content); + article.put(Article.ARTICLE_CONTENT, content); + Stopwatchs.end(); + } + + return article.getString(Article.ARTICLE_CONTENT); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets article content failed[articleId=" + articleId + "]", e); + + throw new ServiceException(e); + } + } + + /** + * Converts the content and abstract for each of the specified articles to HTML if that is saved by Markdown editor. + * + * @param articles the specified articles + */ + public void markdowns(final List articles) { + for (final JSONObject article : articles) { + markdown(article); + } + } + + /** + * Converts the content and abstract for the specified article to HTML if it is saved by Markdown editor. + * + * @param article the specified article + */ + public void markdown(final JSONObject article) { + Stopwatchs.start("Markdown Article [id=" + article.optString(Keys.OBJECT_ID) + "]"); + + String content = article.optString(Article.ARTICLE_CONTENT); + content = Markdowns.toHTML(content); + article.put(Article.ARTICLE_CONTENT, content); + + String abstractContent = article.optString(Article.ARTICLE_ABSTRACT); + if (StringUtils.isNotBlank(abstractContent)) { + Stopwatchs.start("Abstract"); + abstractContent = Markdowns.toHTML(abstractContent); + article.put(Article.ARTICLE_ABSTRACT, abstractContent); + Stopwatchs.end(); + } + + Stopwatchs.end(); + } + + /** + * Removes unused properties of each article in the specified articles. + *

+ * Remains the following properties: + *

    + *
  • {@link Article#ARTICLE_TITLE article title}
  • + *
  • {@link Article#ARTICLE_PERMALINK article permalink}
  • + *
+ *

+ *

+ * The batch version of method {@link #removeUnusedProperties(org.json.JSONObject)}. + *

+ * + * @param articles the specified articles + * @see #removeUnusedProperties(org.json.JSONObject) + */ + private void removeUnusedProperties(final List articles) { + for (final JSONObject article : articles) { + removeUnusedProperties(article); + } + } + + /** + * Removes unused properties of the specified article. + *

+ * Remains the following properties: + *

    + *
  • {@link Article#ARTICLE_TITLE article title}
  • + *
  • {@link Article#ARTICLE_PERMALINK article permalink}
  • + *
+ *

+ * + * @param article the specified article + * @see #removeUnusedProperties(java.util.List) + */ + private void removeUnusedProperties(final JSONObject article) { + article.remove(Keys.OBJECT_ID); + article.remove(Article.ARTICLE_AUTHOR_ID); + article.remove(Article.ARTICLE_ABSTRACT); + article.remove(Article.ARTICLE_COMMENT_COUNT); + article.remove(Article.ARTICLE_CONTENT); + article.remove(Article.ARTICLE_CREATED); + article.remove(Article.ARTICLE_TAGS_REF); + article.remove(Article.ARTICLE_UPDATED); + article.remove(Article.ARTICLE_VIEW_COUNT); + article.remove(Article.ARTICLE_RANDOM_DOUBLE); + article.remove(Article.ARTICLE_PUT_TOP); + article.remove(Article.ARTICLE_VIEW_PWD); + article.remove(Article.ARTICLE_SIGN_ID); + article.remove(Article.ARTICLE_COMMENTABLE); + } +} diff --git a/src/main/java/org/b3log/solo/service/CategoryMgmtService.java b/src/main/java/org/b3log/solo/service/CategoryMgmtService.java new file mode 100644 index 00000000..ed07a2cd --- /dev/null +++ b/src/main/java/org/b3log/solo/service/CategoryMgmtService.java @@ -0,0 +1,261 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.*; +import org.b3log.latke.repository.annotation.Transactional; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Tag; +import org.b3log.solo.repository.CategoryRepository; +import org.b3log.solo.repository.CategoryTagRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Category management service. + * + * @author Liang Ding + * @version 1.2.0.0, Apr 1, 2017 + * @since 2.0.0 + */ +@Service +public class CategoryMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CategoryMgmtService.class); + + /** + * Category repository. + */ + @Inject + private CategoryRepository categoryRepository; + + /** + * Category tag repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * Changes the order of a category specified by the given category id with the specified direction. + * + * @param categoryId the given category id + * @param direction the specified direction, "up"/"down" + * @throws ServiceException service exception + */ + public void changeOrder(final String categoryId, final String direction) + throws ServiceException { + final Transaction transaction = categoryRepository.beginTransaction(); + + try { + final JSONObject srcCategory = categoryRepository.get(categoryId); + final int srcCategoryOrder = srcCategory.getInt(Category.CATEGORY_ORDER); + + JSONObject targetCategory; + + if ("up".equals(direction)) { + targetCategory = categoryRepository.getUpper(categoryId); + } else { // Down + targetCategory = categoryRepository.getUnder(categoryId); + } + + if (null == targetCategory) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.WARN, "Cant not find the target category of source category [order={0}]", srcCategoryOrder); + + return; + } + + // Swaps + srcCategory.put(Category.CATEGORY_ORDER, targetCategory.getInt(Category.CATEGORY_ORDER)); + targetCategory.put(Category.CATEGORY_ORDER, srcCategoryOrder); + + categoryRepository.update(srcCategory.getString(Keys.OBJECT_ID), srcCategory); + categoryRepository.update(targetCategory.getString(Keys.OBJECT_ID), targetCategory); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Changes category's order failed", e); + + throw new ServiceException(e); + } + } + + /** + * Removes a category-tag relation. + * + * @param categoryId the specified category id + * @param tagId the specified tag id + * @throws ServiceException service exception + */ + @Transactional + public void removeCategoryTag(final String categoryId, final String tagId) throws ServiceException { + try { + final JSONObject category = categoryRepository.get(categoryId); + category.put(Category.CATEGORY_TAG_CNT, category.optInt(Category.CATEGORY_TAG_CNT) - 1); + + categoryRepository.update(categoryId, category); + + final Query query = new Query().setFilter( + CompositeFilterOperator.and( + new PropertyFilter(Category.CATEGORY + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, categoryId), + new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId))); + + final JSONArray relations = categoryTagRepository.get(query).optJSONArray(Keys.RESULTS); + if (relations.length() < 1) { + return; + } + + final JSONObject relation = relations.optJSONObject(0); + categoryTagRepository.remove(relation.optString(Keys.OBJECT_ID)); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Adds a category-tag relation failed", e); + + throw new ServiceException(e); + } + } + + /** + * Adds a category-tag relation. + * + * @param categoryTag the specified category-tag relation + * @throws ServiceException service exception + */ + @Transactional + public void addCategoryTag(final JSONObject categoryTag) throws ServiceException { + try { + categoryTagRepository.add(categoryTag); + + final String categoryId = categoryTag.optString(Category.CATEGORY + "_" + Keys.OBJECT_ID); + final JSONObject category = categoryRepository.get(categoryId); + final int tagCount = + categoryTagRepository.getByCategoryId(categoryId, 1, Integer.MAX_VALUE). + optJSONArray(Keys.RESULTS).length(); + category.put(Category.CATEGORY_TAG_CNT, tagCount); + + categoryRepository.update(categoryId, category); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Adds a category-tag relation failed", e); + + throw new ServiceException(e); + } + } + + /** + * Adds a category relation. + * + * @param category the specified category relation + * @return category id + * @throws ServiceException service exception + */ + @Transactional + public String addCategory(final JSONObject category) throws ServiceException { + try { + final JSONObject record = new JSONObject(); + record.put(Category.CATEGORY_TAG_CNT, 0); + record.put(Category.CATEGORY_URI, category.optString(Category.CATEGORY_URI)); + record.put(Category.CATEGORY_TITLE, category.optString(Category.CATEGORY_TITLE)); + record.put(Category.CATEGORY_DESCRIPTION, category.optString(Category.CATEGORY_DESCRIPTION)); + + final int maxOrder = categoryRepository.getMaxOrder(); + final int order = maxOrder + 1; + record.put(Category.CATEGORY_ORDER, order); + category.put(Category.CATEGORY_ORDER, order); + + final String ret = categoryRepository.add(record); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Adds a category failed", e); + + throw new ServiceException(e); + } + } + + /** + * Updates the specified category by the given category id. + * + * @param categoryId the given category id + * @param category the specified category + * @throws ServiceException service exception + */ + @Transactional + public void updateCategory(final String categoryId, final JSONObject category) throws ServiceException { + try { + final JSONObject oldCategory = categoryRepository.get(categoryId); + category.put(Category.CATEGORY_ORDER, oldCategory.optInt(Category.CATEGORY_ORDER)); + category.put(Category.CATEGORY_TAG_CNT, oldCategory.optInt(Category.CATEGORY_TAG_CNT)); + + categoryRepository.update(categoryId, category); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Updates a category [id=" + categoryId + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Removes the specified category by the given category id. + * + * @param categoryId the given category id + * @throws ServiceException service exception + */ + @Transactional + public void removeCategory(final String categoryId) throws ServiceException { + try { + categoryTagRepository.removeByCategoryId(categoryId); + categoryRepository.remove(categoryId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Remove a category [id=" + categoryId + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Removes category-tag relations by the given category id. + * + * @param categoryId the given category id + * @throws ServiceException service exception + */ + @Transactional + public void removeCategoryTags(final String categoryId) throws ServiceException { + try { + categoryTagRepository.removeByCategoryId(categoryId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Remove category-tag [categoryId=" + categoryId + "] failed", e); + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/CategoryQueryService.java b/src/main/java/org/b3log/solo/service/CategoryQueryService.java new file mode 100644 index 00000000..5a1471c1 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/CategoryQueryService.java @@ -0,0 +1,296 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.repository.*; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.CollectionUtils; +import org.b3log.latke.util.Paginator; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Tag; +import org.b3log.solo.repository.CategoryRepository; +import org.b3log.solo.repository.CategoryTagRepository; +import org.b3log.solo.repository.TagRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Category query service. + * + * @author Liang Ding + * @author lzh984294471 + * @version 1.0.1.4, Sep 1, 2019 + * @since 2.0.0 + */ +@Service +public class CategoryQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CategoryQueryService.class); + + /** + * Category repository. + */ + @Inject + private CategoryRepository categoryRepository; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Category tag repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * Gets most tag category. + * + * @param fetchSize the specified fetch size + * @return categories, returns an empty list if not found + */ + public List getMostTagCategory(final int fetchSize) { + final Query query = new Query().addSort(Category.CATEGORY_ORDER, SortDirection.ASCENDING). + addSort(Category.CATEGORY_TAG_CNT, SortDirection.DESCENDING). + addSort(Keys.OBJECT_ID, SortDirection.DESCENDING). + setPageSize(fetchSize).setPageCount(1); + try { + final List ret = categoryRepository.getList(query); + for (final JSONObject category : ret) { + final List tags = getTags(category.optString(Keys.OBJECT_ID)); + + category.put(Category.CATEGORY_T_TAGS, (Object) tags); + } + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets most tag category error", e); + + return Collections.emptyList(); + } + } + + /** + * Gets a category's tags. + * + * @param categoryId the specified category id + * @return tags, returns an empty list if not found + */ + public List getTags(final String categoryId) { + final List ret = new ArrayList<>(); + + final Query query = new Query(). + setFilter(new PropertyFilter(Category.CATEGORY + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, categoryId)); + try { + final List relations = categoryTagRepository.getList(query); + for (final JSONObject relation : relations) { + final String tagId = relation.optString(Tag.TAG + "_" + Keys.OBJECT_ID); + final JSONObject tag = tagRepository.get(tagId); + if (null == tag) { // 修复修改分类时空指针错误 https://github.com/b3log/solo/pull/12876 + continue; + } + ret.add(tag); + } + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets category [id=" + categoryId + "] tags error", e); + } + + return ret; + } + + /** + * Gets a category by the specified category URI. + * + * @param categoryURI the specified category URI + * @return category, returns {@code null} if not null + * @throws ServiceException service exception + */ + public JSONObject getByURI(final String categoryURI) throws ServiceException { + try { + final JSONObject ret = categoryRepository.getByURI(categoryURI); + if (null == ret) { + return null; + } + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets category [URI=" + categoryURI + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Gets a category by the specified category title. + * + * @param categoryTitle the specified category title + * @return category, returns {@code null} if not null + * @throws ServiceException service exception + */ + public JSONObject getByTitle(final String categoryTitle) throws ServiceException { + try { + final JSONObject ret = categoryRepository.getByTitle(categoryTitle); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets category [title=" + categoryTitle + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Gets categories by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "categoryTitle": "", // optional + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10 + * see {@link Pagination} for more details + * @return for example, + *
+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "categories": [{
+     *         "oId": "",
+     *         "categoryTitle": "",
+     *         "categoryDescription": "",
+     *         ....
+     *      }, ....]
+     * }
+     * 
+ * @throws ServiceException service exception + * @see Pagination + */ + public JSONObject getCategoris(final JSONObject requestJSONObject) throws ServiceException { + final JSONObject ret = new JSONObject(); + + final int currentPageNum = requestJSONObject.optInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.optInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.optInt(Pagination.PAGINATION_WINDOW_SIZE); + final Query query = new Query().setPage(currentPageNum, pageSize). + addSort(Category.CATEGORY_ORDER, SortDirection.ASCENDING). + addSort(Category.CATEGORY_TAG_CNT, SortDirection.DESCENDING). + addSort(Keys.OBJECT_ID, SortDirection.DESCENDING); + + if (requestJSONObject.has(Category.CATEGORY_TITLE)) { + query.setFilter(new PropertyFilter(Category.CATEGORY_TITLE, FilterOperator.EQUAL, + requestJSONObject.optString(Category.CATEGORY_TITLE))); + } + + JSONObject result; + try { + result = categoryRepository.get(query); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets categories failed", e); + + throw new ServiceException(e); + } + + final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + + final JSONObject pagination = new JSONObject(); + ret.put(Pagination.PAGINATION, pagination); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + final JSONArray data = result.optJSONArray(Keys.RESULTS); + final List categories = CollectionUtils.jsonArrayToList(data); + + ret.put(Category.CATEGORIES, categories); + + return ret; + } + + /** + * Gets a category by the specified id. + * + * @param categoryId the specified id + * @return a category, return {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getCategory(final String categoryId) throws ServiceException { + try { + final JSONObject ret = categoryRepository.get(categoryId); + if (null == ret) { + return null; + } + + final List tags = getTags(categoryId); + ret.put(Category.CATEGORY_T_TAGS, (Object) tags); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets a category [categoryId=" + categoryId + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Whether a tag specified by the given tag title in a category specified by the given category id. + * + * @param tagTitle the given tag title + * @param categoryId the given category id + * @return {@code true} if the tag in the category, returns {@code false} otherwise + */ + public boolean containTag(final String tagTitle, final String categoryId) { + try { + final JSONObject category = categoryRepository.get(categoryId); + if (null == category) { + return true; + } + + final JSONObject tag = tagRepository.getByTitle(tagTitle); + if (null == tag) { + return true; + } + + final Query query = new Query().setFilter( + CompositeFilterOperator.and( + new PropertyFilter(Category.CATEGORY + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, categoryId), + new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tag.optString(Keys.OBJECT_ID)))); + + return categoryTagRepository.count(query) > 0; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Check category tag [tagTitle=" + tagTitle + ", categoryId=" + categoryId + "] failed", e); + + return true; + } + } +} diff --git a/src/main/java/org/b3log/solo/service/CommentMgmtService.java b/src/main/java/org/b3log/solo/service/CommentMgmtService.java new file mode 100644 index 00000000..a987acb1 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/CommentMgmtService.java @@ -0,0 +1,390 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.event.Event; +import org.b3log.latke.event.EventManager; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Ids; +import org.b3log.latke.util.Strings; +import org.b3log.solo.event.EventTypes; +import org.b3log.solo.model.*; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.CommentRepository; +import org.b3log.solo.repository.UserRepository; +import org.b3log.solo.util.Markdowns; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; + +/** + * Comment management service. + * + * @author Liang Ding + * @version 1.4.0.3, Jun 6, 2019 + * @since 0.3.5 + */ +@Service +public class CommentMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CommentMgmtService.class); + + /** + * Minimum length of comment name. + */ + private static final int MIN_COMMENT_NAME_LENGTH = 2; + + /** + * Maximum length of comment name. + */ + private static final int MAX_COMMENT_NAME_LENGTH = 20; + + /** + * Minimum length of comment content. + */ + private static final int MIN_COMMENT_CONTENT_LENGTH = 2; + + /** + * Maximum length of comment content. + */ + private static final int MAX_COMMENT_CONTENT_LENGTH = 500; + + /** + * Event manager. + */ + @Inject + private static EventManager eventManager; + + /** + * Article management service. + */ + @Inject + private ArticleMgmtService articleMgmtService; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Checks the specified comment adding request. + *

+ * XSS process (name) in this method. + *

+ * + * @param requestJSONObject the specified comment adding request, for example, + * { + * "type": "", // "article" + * "oId": "", + * "commentName": "", + * "commentURL": "", + * "commentContent": "", + * } + * @return check result, for example,
+     * {
+     *     "sc": boolean,
+     *     "msg": "" // Exists if "sc" equals to false
+     * }
+     * 
+ */ + public JSONObject checkAddCommentRequest(final JSONObject requestJSONObject) { + final JSONObject ret = new JSONObject(); + + try { + ret.put(Keys.STATUS_CODE, false); + final JSONObject preference = optionQueryService.getPreference(); + + if (null == preference || !preference.optBoolean(Option.ID_C_COMMENTABLE)) { + ret.put(Keys.MSG, langPropsService.get("notAllowCommentLabel")); + + return ret; + } + + final String id = requestJSONObject.optString(Keys.OBJECT_ID); + final String type = requestJSONObject.optString(Common.TYPE); + + if (Article.ARTICLE.equals(type)) { + final JSONObject article = articleRepository.get(id); + + if (null == article || !article.optBoolean(Article.ARTICLE_COMMENTABLE)) { + ret.put(Keys.MSG, langPropsService.get("notAllowCommentLabel")); + + return ret; + } + } else { + ret.put(Keys.MSG, langPropsService.get("notAllowCommentLabel")); + + return ret; + } + + String commentName = requestJSONObject.getString(Comment.COMMENT_NAME); + if (MAX_COMMENT_NAME_LENGTH < commentName.length() || MIN_COMMENT_NAME_LENGTH > commentName.length()) { + LOGGER.log(Level.WARN, "Comment name is too long [{0}]", commentName); + ret.put(Keys.MSG, langPropsService.get("nameTooLongLabel")); + + return ret; + } + + final JSONObject commenter = userRepository.getByUserName(commentName); + if (null == commenter) { + LOGGER.log(Level.WARN, "Not found user [" + commentName + "]"); + ret.put(Keys.MSG, langPropsService.get("queryUserFailedLabel")); + + return ret; + } + + final String commentURL = requestJSONObject.optString(Comment.COMMENT_URL); + + if (!Strings.isURL(commentURL) || StringUtils.contains(commentURL, "<")) { + requestJSONObject.put(Comment.COMMENT_URL, ""); + } + + String commentContent = requestJSONObject.optString(Comment.COMMENT_CONTENT); + + if (MAX_COMMENT_CONTENT_LENGTH < commentContent.length() || MIN_COMMENT_CONTENT_LENGTH > commentContent.length()) { + LOGGER.log(Level.WARN, "Comment conent length is invalid[{0}]", commentContent.length()); + ret.put(Keys.MSG, langPropsService.get("commentContentCannotEmptyLabel")); + + return ret; + } + + ret.put(Keys.STATUS_CODE, true); + requestJSONObject.put(Comment.COMMENT_CONTENT, commentContent); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.WARN, "Checks add comment request[" + requestJSONObject.toString() + "] failed", e); + + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langPropsService.get("addFailLabel")); + + return ret; + } + } + + /** + * Adds an article comment with the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "oId": "", // article id + * "commentName": "", + * "commentURL": "", // optional + * "commentContent": "", + * "commentOriginalCommentId": "" // optional + * } + * @return add result, for example,
+     * {
+     *     "oId": "", // generated comment id
+     *     "commentDate": "", // format: yyyy-MM-dd HH:mm:ss
+     *     "commentOriginalCommentName": "" // optional, corresponding to argument "commentOriginalCommentId"
+     *     "commentThumbnailURL": "",
+     *     "commentSharpURL": "",
+     *     "commentContent": "",
+     *     "commentName": "",
+     *     "commentURL": "", // optional
+     *     "isReply": boolean,
+     *     "article": {},
+     *     "commentOriginalCommentId": "", // optional
+     *     "commentable": boolean,
+     *     "permalink": "" // article.articlePermalink
+     * }
+     * 
+ * @throws ServiceException service exception + */ + public JSONObject addArticleComment(final JSONObject requestJSONObject) throws ServiceException { + final JSONObject ret = new JSONObject(); + ret.put(Common.IS_REPLY, false); + + final Transaction transaction = commentRepository.beginTransaction(); + + try { + final String articleId = requestJSONObject.getString(Keys.OBJECT_ID); + final JSONObject article = articleRepository.get(articleId); + ret.put(Article.ARTICLE, article); + final String commentName = requestJSONObject.getString(Comment.COMMENT_NAME); + final String commentURL = requestJSONObject.optString(Comment.COMMENT_URL); + final String commentContent = requestJSONObject.getString(Comment.COMMENT_CONTENT); + final String originalCommentId = requestJSONObject.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID); + ret.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, originalCommentId); + final JSONObject comment = new JSONObject(); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, ""); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, ""); + comment.put(Comment.COMMENT_NAME, commentName); + comment.put(Comment.COMMENT_URL, commentURL); + comment.put(Comment.COMMENT_CONTENT, commentContent); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, requestJSONObject.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID)); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, requestJSONObject.optString(Comment.COMMENT_ORIGINAL_COMMENT_NAME)); + final JSONObject preference = optionQueryService.getPreference(); + final Date date = new Date(); + comment.put(Comment.COMMENT_CREATED, date.getTime()); + ret.put(Comment.COMMENT_T_DATE, DateFormatUtils.format(date, "yyyy-MM-dd HH:mm:ss")); + ret.put("commentDate2", date); + ret.put(Common.COMMENTABLE, preference.getBoolean(Option.ID_C_COMMENTABLE) && article.getBoolean(Article.ARTICLE_COMMENTABLE)); + ret.put(Common.PERMALINK, article.getString(Article.ARTICLE_PERMALINK)); + ret.put(Comment.COMMENT_NAME, commentName); + String cmtContent = Markdowns.toHTML(commentContent); + cmtContent = Markdowns.clean(cmtContent); + ret.put(Comment.COMMENT_CONTENT, cmtContent); + ret.put(Comment.COMMENT_URL, commentURL); + + JSONObject originalComment; + if (StringUtils.isNotBlank(originalCommentId)) { + originalComment = commentRepository.get(originalCommentId); + if (null != originalComment) { + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, originalCommentId); + final String originalCommentName = originalComment.getString(Comment.COMMENT_NAME); + + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, originalCommentName); + ret.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, originalCommentName); + + ret.put(Common.IS_REPLY, true); + } else { + LOGGER.log(Level.WARN, "Not found orginal comment[id={0}] of reply[name={1}, content={2}]", + originalCommentId, commentName, commentContent); + } + } + setCommentThumbnailURL(comment); + ret.put(Comment.COMMENT_THUMBNAIL_URL, comment.getString(Comment.COMMENT_THUMBNAIL_URL)); + comment.put(Comment.COMMENT_ON_ID, articleId); + final String commentId = Ids.genTimeMillisId(); + comment.put(Keys.OBJECT_ID, commentId); + ret.put(Keys.OBJECT_ID, commentId); + final String commentSharpURL = Comment.getCommentSharpURLForArticle(article, commentId); + comment.put(Comment.COMMENT_SHARP_URL, commentSharpURL); + ret.put(Comment.COMMENT_SHARP_URL, commentSharpURL); + + commentRepository.add(comment); + articleMgmtService.incArticleCommentCount(articleId); + + final JSONObject eventData = new JSONObject(); + eventData.put(Comment.COMMENT, comment); + eventData.put(Article.ARTICLE, article); + eventManager.fireEventAsynchronously(new Event<>(EventTypes.ADD_COMMENT_TO_ARTICLE, eventData)); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e); + } + + return ret; + } + + /** + * Removes a comment of an article with the specified comment id. + * + * @param commentId the given comment id + * @throws ServiceException service exception + */ + public void removeArticleComment(final String commentId) throws ServiceException { + final Transaction transaction = commentRepository.beginTransaction(); + + try { + final JSONObject comment = commentRepository.get(commentId); + final String articleId = comment.getString(Comment.COMMENT_ON_ID); + commentRepository.remove(commentId); + decArticleCommentCount(articleId); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Removes a comment of an article failed", e); + throw new ServiceException(e); + } + } + + /** + * Article comment count -1 for an article specified by the given article id. + * + * @param articleId the given article id + * @throws JSONException json exception + * @throws RepositoryException repository exception + */ + private void decArticleCommentCount(final String articleId) throws JSONException, RepositoryException { + final JSONObject article = articleRepository.get(articleId); + final JSONObject newArticle = new JSONObject(article, JSONObject.getNames(article)); + final int commentCnt = article.getInt(Article.ARTICLE_COMMENT_COUNT); + newArticle.put(Article.ARTICLE_COMMENT_COUNT, commentCnt - 1); + articleRepository.update(articleId, newArticle, Article.ARTICLE_COMMENT_COUNT); + } + + /** + * Sets commenter thumbnail URL for the specified comment. + * + * @param comment the specified comment + * @throws Exception exception + */ + public void setCommentThumbnailURL(final JSONObject comment) throws Exception { + final String commenterName = comment.optString(Comment.COMMENT_NAME); + final JSONObject commenter = userRepository.getByUserName(commenterName); + final String avatarURL = commenter.optString(UserExt.USER_AVATAR); + comment.put(Comment.COMMENT_THUMBNAIL_URL, avatarURL); + } +} diff --git a/src/main/java/org/b3log/solo/service/CommentQueryService.java b/src/main/java/org/b3log/solo/service/CommentQueryService.java new file mode 100644 index 00000000..766efc53 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/CommentQueryService.java @@ -0,0 +1,265 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.SortDirection; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Paginator; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Comment; +import org.b3log.solo.model.Common; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.CommentRepository; +import org.b3log.solo.repository.PageRepository; +import org.b3log.solo.util.Emotions; +import org.b3log.solo.util.Markdowns; +import org.json.JSONArray; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Comment query service. + * + * @author Liang Ding + * @version 1.3.2.7, Apr 24, 2019 + * @since 0.3.5 + */ +@Service +public class CommentQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CommentQueryService.class); + + /** + * User service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Can the specified user access a comment specified by the given comment id? + * + * @param commentId the given comment id + * @param user the specified user + * @return {@code true} if the current user can access the comment, {@code false} otherwise + * @throws Exception exception + */ + public boolean canAccessComment(final String commentId, final JSONObject user) throws Exception { + if (StringUtils.isBlank(commentId)) { + return false; + } + + if (null == user) { + return false; + } + + if (Role.ADMIN_ROLE.equals(user.optString(User.USER_ROLE))) { + return true; + } + + final JSONObject comment = commentRepository.get(commentId); + if (null == comment) { + return false; + } + + final String onId = comment.optString(Comment.COMMENT_ON_ID); + final JSONObject article = articleRepository.get(onId); + if (null == article) { + return false; + } + + final String currentUserId = user.getString(Keys.OBJECT_ID); + + return article.getString(Article.ARTICLE_AUTHOR_ID).equals(currentUserId); + } + + /** + * Gets comments with the specified request json object, request and response. + * + * @param requestJSONObject the specified request json object, for example, + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10 + * @return for example, + *
+     * {
+     *     "comments": [{
+     *         "oId": "",
+     *         "commentTitle": "",
+     *         "commentName": "",
+     *         "thumbnailUrl": "",
+     *         "commentURL": "",
+     *         "commentContent": "",
+     *         "commentTime": long,
+     *         "commentSharpURL": ""
+     *      }, ....]
+     *     "sc": "GET_COMMENTS_SUCC"
+     * }
+     * 
+ * @throws ServiceException service exception + * @see Pagination + */ + public JSONObject getComments(final JSONObject requestJSONObject) throws ServiceException { + try { + final JSONObject ret = new JSONObject(); + + final int currentPageNum = requestJSONObject.getInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.getInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.getInt(Pagination.PAGINATION_WINDOW_SIZE); + + final Query query = new Query().setPage(currentPageNum, pageSize). + addSort(Comment.COMMENT_CREATED, SortDirection.DESCENDING); + final JSONObject result = commentRepository.get(query); + final JSONArray comments = result.getJSONArray(Keys.RESULTS); + + // Sets comment title and content escaping + for (int i = 0; i < comments.length(); i++) { + final JSONObject comment = comments.getJSONObject(i); + String title; + + final String onId = comment.getString(Comment.COMMENT_ON_ID); + final JSONObject article = articleRepository.get(onId); + if (null == article) { + // 某种情况下导致的数据不一致:文章已经被删除了,但是评论还在 + // 为了保持数据一致性,需要删除该条评论 https://hacpai.com/article/1556060195022 + final Transaction transaction = commentRepository.beginTransaction(); + final String commentId = comment.optString(Keys.OBJECT_ID); + commentRepository.remove(commentId); + transaction.commit(); + + continue; + } + + title = article.getString(Article.ARTICLE_TITLE); + comment.put(Common.TYPE, Common.ARTICLE_COMMENT_TYPE); + comment.put(Common.COMMENT_TITLE, title); + + String commentContent = comment.optString(Comment.COMMENT_CONTENT); + commentContent = Markdowns.toHTML(commentContent); + commentContent = Markdowns.clean(commentContent); + comment.put(Comment.COMMENT_CONTENT, commentContent); + + String commentName = comment.optString(Comment.COMMENT_NAME); + commentName = Jsoup.clean(commentName, Whitelist.none()); + comment.put(Comment.COMMENT_NAME, commentName); + + comment.put(Comment.COMMENT_TIME, comment.optLong(Comment.COMMENT_CREATED)); + comment.remove(Comment.COMMENT_CREATED); + } + + final int pageCount = result.getJSONObject(Pagination.PAGINATION).getInt(Pagination.PAGINATION_PAGE_COUNT); + final JSONObject pagination = new JSONObject(); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + ret.put(Comment.COMMENTS, comments); + ret.put(Pagination.PAGINATION, pagination); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets comments failed", e); + + throw new ServiceException(e); + } + } + + /** + * Gets comments of an article or page specified by the on id. + * + * @param onId the specified on id + * @return a list of comments, returns an empty list if not found + * @throws ServiceException service exception + */ + public List getComments(final String onId) throws ServiceException { + try { + final List ret = new ArrayList<>(); + final List comments = commentRepository.getComments(onId, 1, Integer.MAX_VALUE); + for (final JSONObject comment : comments) { + comment.put(Comment.COMMENT_TIME, comment.optLong(Comment.COMMENT_CREATED)); + comment.put(Comment.COMMENT_T_DATE, new Date(comment.optLong(Comment.COMMENT_CREATED))); + comment.put("commentDate2", new Date(comment.optLong(Comment.COMMENT_CREATED))); // 1.9.0 向后兼容 + comment.put(Comment.COMMENT_NAME, comment.getString(Comment.COMMENT_NAME)); + String url = comment.getString(Comment.COMMENT_URL); + if (StringUtils.contains(url, "<")) { // legacy issue https://github.com/b3log/solo/issues/12091 + url = ""; + } + comment.put(Comment.COMMENT_URL, url); + comment.put(Common.IS_REPLY, false); // Assumes this comment is not a reply + + if (StringUtils.isNotBlank(comment.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID))) { + // This comment is a reply + comment.put(Common.IS_REPLY, true); + } + + String commentContent = comment.optString(Comment.COMMENT_CONTENT); + commentContent = Markdowns.toHTML(commentContent); + commentContent = Markdowns.clean(commentContent); + comment.put(Comment.COMMENT_CONTENT, commentContent); + + String commentName = comment.optString(Comment.COMMENT_NAME); + commentName = Jsoup.clean(commentName, Whitelist.none()); + comment.put(Comment.COMMENT_NAME, commentName); + + ret.add(comment); + } + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets comments failed", e); + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/CronMgmtService.java b/src/main/java/org/b3log/solo/service/CronMgmtService.java new file mode 100644 index 00000000..e4347b08 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/CronMgmtService.java @@ -0,0 +1,128 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Stopwatchs; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Cron management service. + * + * @author Liang Ding + * @version 1.0.0.4, Apr 18, 2019 + * @since 2.9.7 + */ +@Service +public class CronMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(CronMgmtService.class); + + /** + * Cron thread pool. + */ + private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newScheduledThreadPool(1); + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Article management service. + */ + @Inject + private ArticleMgmtService articleMgmtService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Export service. + */ + @Inject + private ExportService exportService; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Start all cron tasks. + */ + public void start() { + long delay = 10000; + + SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { + try { + StatisticMgmtService.removeExpiredOnlineVisitor(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Executes cron failed", e); + } finally { + Stopwatchs.release(); + } + }, delay, 1000 * 60 * 10, TimeUnit.MILLISECONDS); + delay += 2000; + + SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { + try { + articleMgmtService.refreshGitHub(); + userMgmtService.refreshUSite(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Executes cron failed", e); + } finally { + Stopwatchs.release(); + } + }, delay, 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); + delay += 2000; + + SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { + try { + exportService.exportGitHubRepo(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Executes cron failed", e); + } finally { + Stopwatchs.release(); + } + }, delay + 1000 * 60 * 10, 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); + delay += 2000; + + } + + /** + * Stop all cron tasks. + */ + public void stop() { + SCHEDULED_EXECUTOR_SERVICE.shutdown(); + } +} diff --git a/src/main/java/org/b3log/solo/service/DataModelService.java b/src/main/java/org/b3log/solo/service/DataModelService.java new file mode 100644 index 00000000..1077930f --- /dev/null +++ b/src/main/java/org/b3log/solo/service/DataModelService.java @@ -0,0 +1,1104 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import freemarker.template.Template; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.event.Event; +import org.b3log.latke.event.EventManager; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.Plugin; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.plugin.ViewLoadEventData; +import org.b3log.latke.repository.*; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.util.*; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.*; +import org.b3log.solo.repository.*; +import org.b3log.solo.util.Markdowns; +import org.b3log.solo.util.Skins; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; + +import java.io.StringWriter; +import java.util.*; + +import static org.b3log.solo.model.Article.ARTICLE_CONTENT; + +/** + * Data model service. + * + * @author Liang Ding + * @author Liyuan Li + * @version 1.7.0.11, Sep 17, 2019 + * @since 0.3.1 + */ +@Service +public class DataModelService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(DataModelService.class); + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Category repository. + */ + @Inject + private CategoryRepository categoryRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Category-Tag repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * Link repository. + */ + @Inject + private LinkRepository linkRepository; + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Statistic query service. + */ + @Inject + private StatisticQueryService statisticQueryService; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Option query service.. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Article query service. + */ + @Inject + private ArticleQueryService articleQueryService; + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Event manager. + */ + @Inject + private EventManager eventManager; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Fills articles in index.ftl. + * + * @param context the specified HTTP servlet request context + * @param dataModel data model + * @param currentPageNum current page number + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillIndexArticles(final RequestContext context, final Map dataModel, final int currentPageNum, final JSONObject preference) + throws ServiceException { + Stopwatchs.start("Fill Index Articles"); + + try { + final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + + final Query query = new Query().setPage(currentPageNum, pageSize). + setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED)); + + final Template template = Skins.getSkinTemplate(context, "index.ftl"); + boolean isArticles1 = false; + if (null == template) { + LOGGER.debug("The skin dose not contain [index.ftl] template"); + } else // See https://github.com/b3log/solo/issues/179 for more details + if (Templates.hasExpression(template, "<#list articles1 as article>")) { + isArticles1 = true; + query.addSort(Article.ARTICLE_CREATED, SortDirection.DESCENDING); + LOGGER.trace("Query ${articles1} in index.ftl"); + } else { // <#list articles as article> + query.addSort(Article.ARTICLE_PUT_TOP, SortDirection.DESCENDING); + if (preference.getBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)) { + query.addSort(Article.ARTICLE_UPDATED, SortDirection.DESCENDING); + } else { + query.addSort(Article.ARTICLE_CREATED, SortDirection.DESCENDING); + } + } + + final JSONObject articlesResult = articleRepository.get(query); + final List articles = CollectionUtils.jsonArrayToList(articlesResult.optJSONArray(Keys.RESULTS)); + final int pageCount = articlesResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + setArticlesExProperties(context, articles, preference); + + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + if (0 != pageNums.size()) { + dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0)); + dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1)); + } + dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + if (!isArticles1) { + dataModel.put(Article.ARTICLES, articles); + } else { + dataModel.put(Article.ARTICLES + "1", articles); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills index articles failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills links. + * + * @param dataModel data model + * @throws ServiceException service exception + */ + public void fillLinks(final Map dataModel) throws ServiceException { + Stopwatchs.start("Fill Links"); + try { + final Map sorts = new HashMap<>(); + + sorts.put(Link.LINK_ORDER, SortDirection.ASCENDING); + final Query query = new Query().addSort(Link.LINK_ORDER, SortDirection.ASCENDING).setPageCount(1); + final List links = linkRepository.getList(query); + + dataModel.put(Link.LINKS, links); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Fills links failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + Stopwatchs.end(); + } + + /** + * Fills tags. + * + * @param dataModel data model + * @throws ServiceException service exception + */ + public void fillTags(final Map dataModel) throws ServiceException { + Stopwatchs.start("Fill Tags"); + try { + final List tags = tagQueryService.getTags(); + dataModel.put(Tag.TAGS, tags); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills tags failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + + Stopwatchs.end(); + } + + /** + * Fills categories. + * + * @param dataModel data model + * @throws ServiceException service exception + */ + public void fillCategories(final Map dataModel) throws ServiceException { + Stopwatchs.start("Fill Categories"); + + try { + LOGGER.debug("Filling categories...."); + final List categories = categoryRepository.getMostUsedCategories(Integer.MAX_VALUE); + dataModel.put(Category.CATEGORIES, categories); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Fills categories failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills most used categories. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillMostUsedCategories(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Most Used Categories"); + + try { + LOGGER.debug("Filling most used categories...."); + final int mostUsedCategoryDisplayCnt = Integer.MAX_VALUE; // XXX: preference instead + final List categories = categoryRepository.getMostUsedCategories(mostUsedCategoryDisplayCnt); + dataModel.put(Common.MOST_USED_CATEGORIES, categories); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Fills most used categories failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills most used tags. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillMostUsedTags(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Most Used Tags"); + + try { + LOGGER.debug("Filling most used tags...."); + final int mostUsedTagDisplayCnt = preference.getInt(Option.ID_C_MOST_USED_TAG_DISPLAY_CNT); + final List tags = tagArticleRepository.getMostUsedTags(mostUsedTagDisplayCnt); + dataModel.put(Common.MOST_USED_TAGS, tags); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills most used tags failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills archive dates. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillArchiveDates(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Archive Dates"); + + try { + LOGGER.debug("Filling archive dates...."); + final List archiveDates = archiveDateRepository.getArchiveDates(); + final List archiveDates2 = new ArrayList<>(); + dataModel.put(ArchiveDate.ARCHIVE_DATES, archiveDates2); + if (archiveDates.isEmpty()) { + return; + } + + archiveDates2.add(archiveDates.get(0)); + + if (1 < archiveDates.size()) { // XXX: Workaround, remove the duplicated archive dates + for (int i = 1; i < archiveDates.size(); i++) { + final JSONObject archiveDate = archiveDates.get(i); + + final long time = archiveDate.getLong(ArchiveDate.ARCHIVE_TIME); + final String dateString = DateFormatUtils.format(time, "yyyy/MM"); + + final JSONObject last = archiveDates2.get(archiveDates2.size() - 1); + final String lastDateString = DateFormatUtils.format(last.getLong(ArchiveDate.ARCHIVE_TIME), "yyyy/MM"); + + if (!dateString.equals(lastDateString)) { + archiveDates2.add(archiveDate); + } else { + LOGGER.log(Level.DEBUG, "Found a duplicated archive date [{0}]", dateString); + } + } + } + + final String localeString = preference.getString(Option.ID_C_LOCALE_STRING); + final String language = Locales.getLanguage(localeString); + + for (final JSONObject archiveDate : archiveDates2) { + final long time = archiveDate.getLong(ArchiveDate.ARCHIVE_TIME); + final String dateString = DateFormatUtils.format(time, "yyyy/MM"); + final String[] dateStrings = dateString.split("/"); + final String year = dateStrings[0]; + final String month = dateStrings[1]; + + archiveDate.put(ArchiveDate.ARCHIVE_DATE_YEAR, year); + + archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH, month); + if ("en".equals(language)) { + final String monthName = Dates.EN_MONTHS.get(month); + + archiveDate.put(Common.MONTH_NAME, monthName); + } + } + + dataModel.put(ArchiveDate.ARCHIVE_DATES, archiveDates2); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills archive dates failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills most view count articles. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillMostViewCountArticles(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Most View Articles"); + try { + LOGGER.debug("Filling the most view count articles...."); + final int mostCommentArticleDisplayCnt = preference.getInt(Option.ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT); + final List mostViewCountArticles = articleRepository.getMostViewCountArticles(mostCommentArticleDisplayCnt); + + dataModel.put(Common.MOST_VIEW_COUNT_ARTICLES, mostViewCountArticles); + + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills most view count articles failed", e); + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills most comments articles. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillMostCommentArticles(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Most CMMTs Articles"); + + try { + LOGGER.debug("Filling most comment articles...."); + final int mostCommentArticleDisplayCnt = preference.getInt(Option.ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT); + final List mostCommentArticles = articleRepository.getMostCommentArticles(mostCommentArticleDisplayCnt); + + dataModel.put(Common.MOST_COMMENT_ARTICLES, mostCommentArticles); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills most comment articles failed", e); + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills post articles recently. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillRecentArticles(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Recent Articles"); + + try { + final int recentArticleDisplayCnt = preference.getInt(Option.ID_C_RECENT_ARTICLE_DISPLAY_CNT); + final List recentArticles = articleRepository.getRecentArticles(recentArticleDisplayCnt); + dataModel.put(Common.RECENT_ARTICLES, recentArticles); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills recent articles failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills post comments recently. + * + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillRecentComments(final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill Recent Comments"); + try { + LOGGER.debug("Filling recent comments...."); + final int recentCommentDisplayCnt = preference.getInt(Option.ID_C_RECENT_COMMENT_DISPLAY_CNT); + final List recentComments = commentRepository.getRecentComments(recentCommentDisplayCnt); + for (final JSONObject comment : recentComments) { + String commentContent = comment.optString(Comment.COMMENT_CONTENT); + commentContent = Markdowns.toHTML(commentContent); + commentContent = Jsoup.clean(commentContent, Whitelist.relaxed()); + comment.put(Comment.COMMENT_CONTENT, commentContent); + comment.put(Comment.COMMENT_NAME, comment.getString(Comment.COMMENT_NAME)); + comment.put(Comment.COMMENT_URL, comment.getString(Comment.COMMENT_URL)); + comment.put(Common.IS_REPLY, false); + comment.put(Comment.COMMENT_T_DATE, new Date(comment.optLong(Comment.COMMENT_CREATED))); + comment.put("commentDate2", new Date(comment.optLong(Comment.COMMENT_CREATED))); + } + + dataModel.put(Common.RECENT_COMMENTS, recentComments); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills recent comments failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills favicon URL. 可配置 favicon 图标路径 https://github.com/b3log/solo/issues/12706 + * + * @param dataModel the specified data model + * @param preference the specified preference + */ + public void fillFaviconURL(final Map dataModel, final JSONObject preference) { + if (null == preference) { + dataModel.put(Common.FAVICON_URL, Option.DefaultPreference.DEFAULT_FAVICON_URL); + } else { + dataModel.put(Common.FAVICON_URL, preference.optString(Option.ID_C_FAVICON_URL)); + } + } + + /** + * Fills usite. 展示站点连接 https://github.com/b3log/solo/issues/12719 + * + * @param dataModel the specified data model + */ + public void fillUsite(final Map dataModel) { + try { + final JSONObject usiteOpt = optionQueryService.getOptionById(Option.ID_C_USITE); + if (null == usiteOpt) { + return; + } + + dataModel.put(Option.ID_C_USITE, new JSONObject(usiteOpt.optString(Option.OPTION_VALUE))); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills usite failed", e); + } + } + + /** + * Fills common parts (header, side and footer). + * + * @param context the specified HTTP servlet request context + * @param dataModel the specified data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillCommon(final RequestContext context, final Map dataModel, final JSONObject preference) throws ServiceException { + fillSide(context, dataModel, preference); + fillBlogHeader(context, dataModel, preference); + fillBlogFooter(context, dataModel, preference); + + // 支持配置自定义模板变量 https://github.com/b3log/solo/issues/12535 + final Map customVars = new HashMap<>(); + final String customVarsStr = preference.optString(Option.ID_C_CUSTOM_VARS); + final String[] customVarsArray = customVarsStr.split("\\|"); + for (int i = 0; i < customVarsArray.length; i++) { + final String customVarPair = customVarsArray[i]; + if (StringUtils.isNotBlank(customVarsStr)) { + final String customVarKey = customVarPair.split("=")[0]; + final String customVarVal = customVarPair.split("=")[1]; + if (StringUtils.isNotBlank(customVarKey) && StringUtils.isNotBlank(customVarVal)) { + customVars.put(customVarKey, customVarVal); + } + } + } + dataModel.put("customVars", customVars); + + dataModel.put(Common.LUTE_AVAILABLE, Markdowns.LUTE_AVAILABLE); + String hljsTheme = preference.optString(Option.ID_C_HLJS_THEME); + if (StringUtils.isBlank(hljsTheme)) { + hljsTheme = Option.DefaultPreference.DEFAULT_HLJS_THEME; + } + dataModel.put(Option.ID_C_HLJS_THEME, hljsTheme); + } + + /** + * Fills footer.ftl. + * + * @param context the specified HTTP servlet request context + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + private void fillBlogFooter(final RequestContext context, final Map dataModel, final JSONObject preference) + throws ServiceException { + Stopwatchs.start("Fill Footer"); + try { + LOGGER.debug("Filling footer...."); + final String blogTitle = preference.getString(Option.ID_C_BLOG_TITLE); + dataModel.put(Option.ID_C_BLOG_TITLE, blogTitle); + dataModel.put("blogHost", Latkes.getServePath()); + dataModel.put(Common.VERSION, SoloServletListener.VERSION); + dataModel.put(Common.STATIC_RESOURCE_VERSION, Latkes.getStaticResourceVersion()); + dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR))); + String footerContent = ""; + final JSONObject opt = optionQueryService.getOptionById(Option.ID_C_FOOTER_CONTENT); + if (null != opt) { + footerContent = opt.optString(Option.OPTION_VALUE); + } + dataModel.put(Option.ID_C_FOOTER_CONTENT, footerContent); + dataModel.put(Keys.Server.STATIC_SERVER, Latkes.getStaticServer()); + dataModel.put(Keys.Server.SERVER, Latkes.getServer()); + dataModel.put(Common.IS_INDEX, "/".equals(context.requestURI())); + dataModel.put(User.USER_NAME, ""); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + if (null != currentUser) { + final String userAvatar = currentUser.optString(UserExt.USER_AVATAR); + dataModel.put(Common.GRAVATAR, userAvatar); + dataModel.put(User.USER_NAME, currentUser.optString(User.USER_NAME)); + } + + // Activates plugins + final ViewLoadEventData data = new ViewLoadEventData(); + data.setViewName("footer.ftl"); + data.setDataModel(dataModel); + eventManager.fireEventSynchronously(new Event<>(Keys.FREEMARKER_ACTION, data)); + if (StringUtils.isBlank((String) dataModel.get(Plugin.PLUGINS))) { + // There is no plugin for this template, fill ${plugins} with blank. + dataModel.put(Plugin.PLUGINS, ""); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills blog footer failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills header.ftl. + * + * @param context the specified HTTP servlet request context + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + private void fillBlogHeader(final RequestContext context, final Map dataModel, final JSONObject preference) + throws ServiceException { + Stopwatchs.start("Fill Header"); + try { + LOGGER.debug("Filling header...."); + final String topBarHTML = getTopBarHTML(context); + dataModel.put(Common.LOGIN_URL, userQueryService.getLoginURL(Common.ADMIN_INDEX_URI)); + dataModel.put(Common.LOGOUT_URL, userQueryService.getLogoutURL()); + dataModel.put(Common.ONLINE_VISITOR_CNT, StatisticQueryService.getOnlineVisitorCount()); + dataModel.put(Common.TOP_BAR, topBarHTML); + dataModel.put(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT, preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT)); + dataModel.put(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE, preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE)); + dataModel.put(Option.ID_C_LOCALE_STRING, preference.getString(Option.ID_C_LOCALE_STRING)); + dataModel.put(Option.ID_C_BLOG_TITLE, preference.getString(Option.ID_C_BLOG_TITLE)); + dataModel.put(Option.ID_C_BLOG_SUBTITLE, preference.getString(Option.ID_C_BLOG_SUBTITLE)); + dataModel.put(Option.ID_C_HTML_HEAD, preference.getString(Option.ID_C_HTML_HEAD)); + String metaKeywords = preference.getString(Option.ID_C_META_KEYWORDS); + if (StringUtils.isBlank(metaKeywords)) { + metaKeywords = ""; + } + dataModel.put(Option.ID_C_META_KEYWORDS, metaKeywords); + String metaDescription = preference.getString(Option.ID_C_META_DESCRIPTION); + if (StringUtils.isBlank(metaDescription)) { + metaDescription = ""; + } + dataModel.put(Option.ID_C_META_DESCRIPTION, metaDescription); + dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR))); + dataModel.put(Common.IS_LOGGED_IN, null != Solos.getCurrentUser(context.getRequest(), context.getResponse())); + dataModel.put(Common.FAVICON_API, Solos.FAVICON_API); + final String noticeBoard = preference.getString(Option.ID_C_NOTICE_BOARD); + dataModel.put(Option.ID_C_NOTICE_BOARD, noticeBoard); + // 皮肤不显示访客用户 https://github.com/b3log/solo/issues/12752 + final Query query = new Query().setPageCount(1).setFilter(new PropertyFilter(User.USER_ROLE, FilterOperator.NOT_EQUAL, Role.VISITOR_ROLE)); + final List userList = userRepository.getList(query); + dataModel.put(User.USERS, userList); + final JSONObject admin = userRepository.getAdmin(); + dataModel.put(Common.ADMIN_USER, admin); + final String skinDirName = (String) context.attr(Keys.TEMAPLTE_DIR_NAME); + dataModel.put(Option.ID_C_SKIN_DIR_NAME, skinDirName); + Keys.fillRuntime(dataModel); + fillMinified(dataModel); + fillPageNavigations(dataModel); + fillStatistic(dataModel); + fillMostUsedTags(dataModel, preference); + fillArchiveDates(dataModel, preference); + fillMostUsedCategories(dataModel, preference); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills blog header failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills minified directory and file postfix for static JavaScript, CSS. + * + * @param dataModel the specified data model + */ + public void fillMinified(final Map dataModel) { + switch (Latkes.getRuntimeMode()) { + case DEVELOPMENT: + dataModel.put(Common.MINI_POSTFIX, ""); + break; + + case PRODUCTION: + dataModel.put(Common.MINI_POSTFIX, Common.MINI_POSTFIX_VALUE); + break; + + default: + throw new AssertionError(); + } + } + + /** + * Fills side.ftl. + * + * @param context the specified HTTP servlet request context + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + private void fillSide(final RequestContext context, final Map dataModel, final JSONObject preference) + throws ServiceException { + Stopwatchs.start("Fill Side"); + try { + LOGGER.debug("Filling side...."); + + Template template = Skins.getSkinTemplate(context, "side.ftl"); + if (null == template) { + LOGGER.debug("The skin dose not contain [side.ftl] template"); + + template = Skins.getSkinTemplate(context, "index.ftl"); + if (null == template) { + LOGGER.debug("The skin dose not contain [index.ftl] template"); + return; + } + } + + if (Templates.hasExpression(template, "<#list recentArticles as article>")) { + fillRecentArticles(dataModel, preference); + } + + if (Templates.hasExpression(template, "<#list links as link>")) { + fillLinks(dataModel); + } + + if (Templates.hasExpression(template, "<#list recentComments as comment>")) { + fillRecentComments(dataModel, preference); + } + + if (Templates.hasExpression(template, "<#list mostCommentArticles as article>")) { + fillMostCommentArticles(dataModel, preference); + } + + if (Templates.hasExpression(template, "<#list mostViewCountArticles as article>")) { + fillMostViewCountArticles(dataModel, preference); + } + } catch (final ServiceException e) { + LOGGER.log(Level.ERROR, "Fills side failed", e); + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills the specified template. + * + * @param context the specified HTTP servlet request context + * @param template the specified template + * @param dataModel data model + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void fillUserTemplate(final RequestContext context, final Template template, + final Map dataModel, final JSONObject preference) throws ServiceException { + Stopwatchs.start("Fill User Template[name=" + template.getName() + "]"); + try { + LOGGER.log(Level.DEBUG, "Filling user template[name{0}]", template.getName()); + + if (Templates.hasExpression(template, "<#list links as link>")) { + fillLinks(dataModel); + } + + if (Templates.hasExpression(template, "<#list tags as tag>")) { + fillTags(dataModel); + } + + if (Templates.hasExpression(template, "<#list categories as category>")) { + fillCategories(dataModel); + } + + if (Templates.hasExpression(template, "<#list recentComments as comment>")) { + fillRecentComments(dataModel, preference); + } + + if (Templates.hasExpression(template, "<#list mostCommentArticles as article>")) { + fillMostCommentArticles(dataModel, preference); + } + + if (Templates.hasExpression(template, "<#list mostViewCountArticles as article>")) { + fillMostViewCountArticles(dataModel, preference); + } + + if (Templates.hasExpression(template, "<#include \"side.ftl\"/>")) { + fillSide(context, dataModel, preference); + } + + final String noticeBoard = preference.getString(Option.ID_C_NOTICE_BOARD); + + dataModel.put(Option.ID_C_NOTICE_BOARD, noticeBoard); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills user template failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills page navigations. + * + * @param dataModel data model + * @throws ServiceException service exception + */ + private void fillPageNavigations(final Map dataModel) throws ServiceException { + Stopwatchs.start("Fill Navigations"); + try { + LOGGER.debug("Filling page navigations...."); + final List pages = pageRepository.getPages(); + dataModel.put(Common.PAGE_NAVIGATIONS, pages); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Fills page navigations failed", e); + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Fills statistic. + * + * @param dataModel the specified data model + */ + private void fillStatistic(final Map dataModel) { + Stopwatchs.start("Fill Statistic"); + try { + LOGGER.debug("Filling statistic...."); + final JSONObject statistic = statisticQueryService.getStatistic(); + dataModel.put(Option.CATEGORY_C_STATISTIC, statistic); + } finally { + Stopwatchs.end(); + } + } + + /** + * Sets some extra properties into the specified article with the specified preference, performs content and abstract editor processing. + *

+ * Article ext properties: + *

+     * {
+     *     ....,
+     *     "authorName": "",
+     *     "authorId": "",
+     *     "authorThumbnailURL": "",
+     *     "hasUpdated": boolean
+     * }
+     * 
+ *

+ * + * @param context the specified HTTP servlet request context + * @param article the specified article + * @param preference the specified preference + * @throws ServiceException service exception + * @see #setArticlesExProperties(RequestContext, List, JSONObject) + */ + private void setArticleExProperties(final RequestContext context, final JSONObject article, final JSONObject preference) throws ServiceException { + try { + final JSONObject author = articleQueryService.getAuthor(article); + final String authorName = author.getString(User.USER_NAME); + article.put(Common.AUTHOR_NAME, authorName); + final String authorId = author.getString(Keys.OBJECT_ID); + article.put(Common.AUTHOR_ID, authorId); + article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.optLong(Article.ARTICLE_CREATED))); + article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED))); + + final String userAvatar = author.optString(UserExt.USER_AVATAR); + article.put(Common.AUTHOR_THUMBNAIL_URL, userAvatar); + + if (preference.getBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)) { + article.put(Common.HAS_UPDATED, articleQueryService.hasUpdated(article)); + } else { + article.put(Common.HAS_UPDATED, false); + } + + if (Solos.needViewPwd(context, article)) { + final String content = langPropsService.get("articleContentPwd"); + article.put(ARTICLE_CONTENT, content); + } + + processArticleAbstract(preference, article); + + articleQueryService.markdown(article); + + fillCategory(article); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Sets article extra properties failed", e); + throw new ServiceException(e); + } + } + + /** + * Fills category for the specified article. + * + * @param article the specified article + */ + public void fillCategory(final JSONObject article) { + final String tagsStr = article.optString(Article.ARTICLE_TAGS_REF); + final String[] tags = tagsStr.split(","); + JSONObject category = null; + for (final String tagTitle : tags) { + final JSONObject c = getCategoryOfTag(tagTitle); + if (null != c) { + category = c; + break; + } + } + article.put(Category.CATEGORY, category); + } + + /** + * Gets a category for a tag specified by the given tag title. + * + * @param tagTitle the given tag title + * @return category, returns {@code null} if not found + */ + private JSONObject getCategoryOfTag(final String tagTitle) { + try { + final JSONObject tag = tagRepository.getByTitle(tagTitle); + if (null == tag) { + return null; + } + + final String tagId = tag.optString(Keys.OBJECT_ID); + final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)). + setPage(1, 1).setPageCount(1); + final JSONObject tagCategory = categoryTagRepository.getFirst(query); + if (null == tagCategory) { + return null; + } + + final String categoryId = tagCategory.optString(Category.CATEGORY + "_" + Keys.OBJECT_ID); + + return categoryRepository.get(categoryId); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets category of tag [" + tagTitle + "] failed", e); + + return null; + } + } + + /** + * Sets some extra properties into the specified article with the specified preference. + *

+ * The batch version of method {@linkplain #setArticleExProperties(RequestContext, JSONObject, JSONObject)}. + *

+ *

+ * Article ext properties: + *

+     * {
+     *     ....,
+     *     "authorName": "",
+     *     "authorId": "",
+     *     "hasUpdated": boolean
+     * }
+     * 
+ *

+ * + * @param context the specified HTTP servlet request context + * @param articles the specified articles + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void setArticlesExProperties(final RequestContext context, final List articles, final JSONObject preference) + throws ServiceException { + for (final JSONObject article : articles) { + setArticleExProperties(context, article, preference); + } + } + + /** + * Processes the abstract of the specified article with the specified preference. + *
    + *
  • If the abstract is {@code null}, sets it with ""
  • + *
  • If user configured preference "titleOnly", sets the abstract with ""
  • + *
  • If user configured preference "titleAndContent", sets the abstract with the content of the article
  • + *
+ * + * @param preference the specified preference + * @param article the specified article + */ + private void processArticleAbstract(final JSONObject preference, final JSONObject article) { + final String articleAbstract = article.optString(Article.ARTICLE_ABSTRACT); + if (StringUtils.isBlank(articleAbstract)) { + article.put(Article.ARTICLE_ABSTRACT, ""); + } + final String articleAbstractText = article.optString(Article.ARTICLE_ABSTRACT_TEXT); + if (StringUtils.isBlank(articleAbstractText)) { + // 发布文章时会自动提取摘要文本,其中如果文章加密且没有写摘要,则自动提取文本会返回空字符串 Article#getAbstractText() + // 所以当且仅当文章加密且没有摘要的情况下 articleAbstractText 会为空 + final LangPropsService langPropsService = BeanManager.getInstance().getReference(LangPropsService.class); + article.put(Article.ARTICLE_ABSTRACT_TEXT, langPropsService.get("articleContentPwd")); + } + + final String articleListStyle = preference.optString(Option.ID_C_ARTICLE_LIST_STYLE); + if ("titleOnly".equals(articleListStyle)) { + article.put(Article.ARTICLE_ABSTRACT, ""); + } else if ("titleAndContent".equals(articleListStyle)) { + article.put(Article.ARTICLE_ABSTRACT, article.optString(Article.ARTICLE_CONTENT)); + } + } + + /** + * Generates top bar HTML. + * + * @param context the specified request context + * @return top bar HTML + * @throws ServiceException service exception + */ + public String getTopBarHTML(final RequestContext context) throws ServiceException { + Stopwatchs.start("Gens Top Bar HTML"); + + try { + final Template topBarTemplate = Skins.getTemplate("common-template/top-bar.ftl"); + final StringWriter stringWriter = new StringWriter(); + final Map topBarModel = new HashMap<>(); + final JSONObject currentUser = Solos.getCurrentUser(context.getRequest(), context.getResponse()); + + Keys.fillServer(topBarModel); + topBarModel.put(Common.IS_LOGGED_IN, false); + topBarModel.put(Common.IS_MOBILE_REQUEST, Solos.isMobile(context.getRequest())); + topBarModel.put("mobileLabel", langPropsService.get("mobileLabel")); + topBarModel.put("onlineVisitor1Label", langPropsService.get("onlineVisitor1Label")); + topBarModel.put(Common.ONLINE_VISITOR_CNT, StatisticQueryService.getOnlineVisitorCount()); + if (null == currentUser) { + topBarModel.put(Common.LOGIN_URL, userQueryService.getLoginURL(Common.ADMIN_INDEX_URI)); + topBarModel.put("startToUseLabel", langPropsService.get("startToUseLabel")); + topBarTemplate.process(topBarModel, stringWriter); + + return stringWriter.toString(); + } + + topBarModel.put(Common.IS_LOGGED_IN, true); + topBarModel.put(Common.LOGOUT_URL, userQueryService.getLogoutURL()); + topBarModel.put(Common.IS_ADMIN, Role.ADMIN_ROLE.equals(currentUser.getString(User.USER_ROLE))); + topBarModel.put(Common.IS_VISITOR, Role.VISITOR_ROLE.equals(currentUser.getString(User.USER_ROLE))); + topBarModel.put("adminLabel", langPropsService.get("adminLabel")); + topBarModel.put("logoutLabel", langPropsService.get("logoutLabel")); + final String userName = currentUser.getString(User.USER_NAME); + topBarModel.put(User.USER_NAME, userName); + topBarTemplate.process(topBarModel, stringWriter); + + return stringWriter.toString(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gens top bar HTML failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/ExportService.java b/src/main/java/org/b3log/solo/service/ExportService.java new file mode 100644 index 00000000..639037e5 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/ExportService.java @@ -0,0 +1,415 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.io.ZipUtil; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Plugin; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.*; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Strings; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.*; +import org.b3log.solo.repository.*; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Export service. + * + * @author Liang Ding + * @version 1.1.1.1, Sep 18, 2019 + * @since 2.5.0 + */ +@Service +public class ExportService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ExportService.class); + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Category repository. + */ + @Inject + private CategoryRepository categoryRepository; + + /** + * Category-Tag relation repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Link repository. + */ + @Inject + private LinkRepository linkRepository; + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Plugin repository. + */ + @Inject + private PluginRepository pluginRepository; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Exports public articles to admin's GitHub repos. 博文定时同步 GitHub 仓库 https://hacpai.com/article/1557238327458 + */ + public void exportGitHubRepo() { + LOGGER.log(Level.INFO, "Github repo syncing...."); + try { + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + return; + } + + if (!preference.optBoolean(Option.ID_C_SYNC_GITHUB)) { + return; + } + + if (Latkes.getServePath().contains("localhost") || Strings.isIPv4(Latkes.getServerHost())) { + return; + } + + if (Latkes.RuntimeMode.PRODUCTION != Latkes.getRuntimeMode()) { + return; + } + + final JSONObject mds = exportHexoMDs(); + final List posts = (List) mds.opt("posts"); + + final String tmpDir = System.getProperty("java.io.tmpdir"); + final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"); + String localFilePath = tmpDir + File.separator + "solo-hexo-" + date; + final File localFile = new File(localFilePath); + + final File postDir = new File(localFilePath + File.separator + "posts"); + exportHexoMd(posts, postDir.getPath()); + + final File zipFile = ZipUtil.zip(localFile); + byte[] zipData; + try (final FileInputStream inputStream = new FileInputStream(zipFile)) { + zipData = IOUtils.toByteArray(inputStream); + } + + FileUtils.deleteQuietly(localFile); + FileUtils.deleteQuietly(zipFile); + + final JSONObject user = userRepository.getAdmin(); + final String userName = user.optString(User.USER_NAME); + final String userB3Key = user.optString(UserExt.USER_B3_KEY); + final String clientTitle = preference.optString(Option.ID_C_BLOG_TITLE); + final String clientSubtitle = preference.optString(Option.ID_C_BLOG_SUBTITLE); + + final Set articleIds = new HashSet<>(); + final Filter published = new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED); + + final StringBuilder bodyBuilder = new StringBuilder("### 最新\n"); + final List recentArticles = articleRepository.getList(new Query().setFilter(published).select(Keys.OBJECT_ID, Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK).addSort(Article.ARTICLE_CREATED, SortDirection.DESCENDING).setPage(1, 20)); + for (final JSONObject article : recentArticles) { + final String title = article.optString(Article.ARTICLE_TITLE); + final String link = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK); + bodyBuilder.append("\n* [").append(title).append("](").append(link).append(")"); + articleIds.add(article.optString(Keys.OBJECT_ID)); + } + bodyBuilder.append("\n\n"); + + final StringBuilder mostViewBuilder = new StringBuilder(); + final List mostViewArticles = articleRepository.getList(new Query().setFilter(published).select(Keys.OBJECT_ID, Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK).addSort(Article.ARTICLE_VIEW_COUNT, SortDirection.DESCENDING).setPage(1, 40)); + int count = 0; + for (final JSONObject article : mostViewArticles) { + final String articleId = article.optString(Keys.OBJECT_ID); + if (!articleIds.contains(articleId)) { + final String title = article.optString(Article.ARTICLE_TITLE); + final String link = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK); + mostViewBuilder.append("\n* [").append(title).append("](").append(link).append(")"); + articleIds.add(articleId); + count++; + } + if (20 <= count) { + break; + } + } + if (0 < mostViewBuilder.length()) { + bodyBuilder.append("### 热门\n").append(mostViewBuilder).append("\n\n"); + } + + final StringBuilder mostCmtBuilder = new StringBuilder(); + final List mostCmtArticles = articleRepository.getList(new Query().setFilter(published).select(Keys.OBJECT_ID, Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK).addSort(Article.ARTICLE_COMMENT_COUNT, SortDirection.DESCENDING).setPage(1, 60)); + count = 0; + for (final JSONObject article : mostCmtArticles) { + final String articleId = article.optString(Keys.OBJECT_ID); + if (!articleIds.contains(articleId)) { + final String title = article.optString(Article.ARTICLE_TITLE); + final String link = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK); + mostCmtBuilder.append("\n* [").append(title).append("](").append(link).append(")"); + articleIds.add(articleId); + count++; + } + if (20 <= count) { + break; + } + } + if (0 < mostCmtBuilder.length()) { + bodyBuilder.append("### 热议\n").append(mostCmtBuilder); + } + + final HttpResponse response = HttpRequest.post("https://hacpai.com/github/repos"). + connectionTimeout(7000).timeout(60000).trustAllCerts(true).header("User-Agent", Solos.USER_AGENT). + form("userName", userName, + "userB3Key", userB3Key, + "clientName", "Solo", + "clientVersion", SoloServletListener.VERSION, + "clientHost", Latkes.getServePath(), + "clientFavicon", preference.optString(Option.ID_C_FAVICON_URL), + "clientTitle", clientTitle, + "clientSubtitle", clientSubtitle, + "clientBody", bodyBuilder.toString(), + "file", zipData).send(); + response.close(); + response.charset("UTF-8"); + LOGGER.info("Github repo sync completed: " + response.bodyText()); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Exports articles to github repo failed", e); + } finally { + LOGGER.log(Level.INFO, "Github repo synced"); + } + } + + /** + * Exports the specified articles to the specified dir path. + * + * @param articles the specified articles + * @param dirPath the specified dir path + */ + public void exportHexoMd(final List articles, final String dirPath) { + articles.forEach(article -> { + final String filename = Solos.sanitizeFilename(article.optString("title")) + ".md"; + final String text = article.optString("front") + "---" + Strings.LINE_SEPARATOR + article.optString("content"); + + try { + final String date = DateFormatUtils.format(article.optLong("created"), "yyyyMM"); + final String dir = dirPath + File.separator + date + File.separator; + new File(dir).mkdirs(); + FileUtils.writeStringToFile(new File(dir + filename), text, "UTF-8"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Write markdown file failed", e); + } + }); + } + + /** + * Exports as Hexo markdown format. + * + * @return posts, password posts and drafts,
+     * {
+     *     "posts": [
+     *         {
+     *             "front": "", // yaml front matter,
+     *             "title": "",
+     *             "content": "",
+     *             "created": long
+     *         }, ....
+     *     ],
+     *     "passwords": [], // format is same as post
+     *     "drafts": [] // format is same as post
+     * }
+     * 
+ */ + public JSONObject exportHexoMDs() { + final JSONObject ret = new JSONObject(); + final List posts = new ArrayList<>(); + ret.put("posts", (Object) posts); + final List passwords = new ArrayList<>(); + ret.put("passwords", (Object) passwords); + final List drafts = new ArrayList<>(); + ret.put("drafts", (Object) drafts); + + final JSONArray articles = getJSONs(articleRepository); + for (int i = 0; i < articles.length(); i++) { + final JSONObject article = articles.optJSONObject(i); + final Map front = new LinkedHashMap<>(); + final String title = article.optString(Article.ARTICLE_TITLE); + front.put("title", title); + final String date = DateFormatUtils.format(article.optLong(Article.ARTICLE_CREATED), "yyyy-MM-dd HH:mm:ss"); + front.put("date", date); + front.put("updated", DateFormatUtils.format(article.optLong(Article.ARTICLE_UPDATED), "yyyy-MM-dd HH:mm:ss")); + final List tags = Arrays.stream(article.optString(Article.ARTICLE_TAGS_REF).split(",")).filter(StringUtils::isNotBlank).map(String::trim).collect(Collectors.toList()); + if (tags.isEmpty()) { + tags.add("Solo"); + } + front.put("tags", tags); + front.put("permalink", article.optString(Article.ARTICLE_PERMALINK)); + final JSONObject one = new JSONObject(); + one.put("front", new Yaml().dump(front)); + one.put("title", title); + one.put("content", article.optString(Article.ARTICLE_CONTENT)); + one.put("created", article.optLong(Article.ARTICLE_CREATED)); + + if (StringUtils.isNotBlank(article.optString(Article.ARTICLE_VIEW_PWD))) { + passwords.add(one); + + continue; + } else if (Article.ARTICLE_STATUS_C_PUBLISHED == article.optInt(Article.ARTICLE_STATUS)) { + posts.add(one); + + continue; + } else { + drafts.add(one); + } + } + + return ret; + } + + /** + * Gets all data as JSON format. + */ + public JSONObject getJSONs() { + final JSONObject ret = new JSONObject(); + final JSONArray archiveDates = getJSONs(archiveDateRepository); + ret.put(ArchiveDate.ARCHIVE_DATES, archiveDates); + + final JSONArray archiveDateArticles = getJSONs(archiveDateArticleRepository); + ret.put(ArchiveDate.ARCHIVE_DATE + "_" + Article.ARTICLE, archiveDateArticles); + + final JSONArray articles = getJSONs(articleRepository); + ret.put(Article.ARTICLES, articles); + + final JSONArray categories = getJSONs(categoryRepository); + ret.put(Category.CATEGORIES, categories); + + final JSONArray categoryTags = getJSONs(categoryTagRepository); + ret.put(Category.CATEGORY + "_" + Tag.TAG, categoryTags); + + final JSONArray comments = getJSONs(commentRepository); + ret.put(Comment.COMMENTS, comments); + + final JSONArray links = getJSONs(linkRepository); + ret.put(Link.LINKS, links); + + final JSONArray options = getJSONs(optionRepository); + ret.put(Option.OPTIONS, options); + + final JSONArray pages = getJSONs(pageRepository); + ret.put(Page.PAGES, pages); + + final JSONArray plugins = getJSONs(pluginRepository); + ret.put(Plugin.PLUGINS, plugins); + + final JSONArray tags = getJSONs(tagRepository); + ret.put(Tag.TAGS, tags); + + final JSONArray tagArticles = getJSONs(tagArticleRepository); + ret.put(Tag.TAG + "_" + Article.ARTICLES, tagArticles); + + final JSONArray users = getJSONs(userRepository); + ret.put(User.USERS, users); + + return ret; + } + + private JSONArray getJSONs(final Repository repository) { + try { + return repository.get(new Query()).optJSONArray(Keys.RESULTS); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets data from repository [" + repository.getName() + "] failed", e); + + return new JSONArray(); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/ImportService.java b/src/main/java/org/b3log/solo/service/ImportService.java new file mode 100644 index 00000000..0a3cbfaf --- /dev/null +++ b/src/main/java/org/b3log/solo/service/ImportService.java @@ -0,0 +1,289 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Strings; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Article; +import org.json.JSONObject; +import org.yaml.snakeyaml.Yaml; + +import javax.servlet.ServletContext; +import java.io.File; +import java.util.*; + +/** + * Import service. + * + * @author Liang Ding + * @version 1.0.1.5, Mar 20, 2019 + * @since 2.2.0 + */ +@Service +public class ImportService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(ImportService.class); + + /** + * Default tag. + */ + private static final String DEFAULT_TAG = "Note"; + + /** + * Article management service. + */ + @Inject + private ArticleMgmtService articleMgmtService; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Imports markdowns files as articles. See Solo 支持 Hexo/Jekyll 数据导入 for + * more details. + */ + public void importMarkdowns() { + new Thread(() -> { + final ServletContext servletContext = SoloServletListener.getServletContext(); + final String markdownsPath = servletContext.getRealPath("markdowns"); + LOGGER.debug("Import directory [" + markdownsPath + "]"); + + JSONObject admin; + try { + admin = userQueryService.getAdmin(); + } catch (final Exception e) { + return; + } + + if (null == admin) { // Not init yet + return; + } + + final String adminId = admin.optString(Keys.OBJECT_ID); + + int succCnt = 0, failCnt = 0; + final Set failSet = new TreeSet<>(); + final Collection mds = FileUtils.listFiles(new File(markdownsPath), new String[]{"md"}, true); + if (null == mds || mds.isEmpty()) { + return; + } + + for (final File md : mds) { + final String fileName = md.getName(); + if (StringUtils.equalsIgnoreCase(fileName, "README.md")) { + continue; + } + + try { + final String fileContent = FileUtils.readFileToString(md, "UTF-8"); + final JSONObject article = parseArticle(fileName, fileContent); + article.put(Article.ARTICLE_AUTHOR_ID, adminId); + + final JSONObject request = new JSONObject(); + request.put(Article.ARTICLE, article); + + final String id = articleMgmtService.addArticle(request); + FileUtils.moveFile(md, new File(md.getPath() + "." + id)); + LOGGER.info("Imported article [" + article.optString(Article.ARTICLE_TITLE) + "]"); + succCnt++; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Import file [" + fileName + "] failed", e); + + failCnt++; + failSet.add(fileName); + } + } + + if (0 == succCnt && 0 == failCnt) { + return; + } + + final StringBuilder logBuilder = new StringBuilder(); + logBuilder.append("[").append(succCnt).append("] imported, [").append(failCnt).append("] failed"); + if (failCnt > 0) { + logBuilder.append(": ").append(Strings.LINE_SEPARATOR); + + for (final String fail : failSet) { + logBuilder.append(" ").append(fail).append(Strings.LINE_SEPARATOR); + } + } else { + logBuilder.append(" :p"); + } + LOGGER.info(logBuilder.toString()); + }).start(); + } + + private JSONObject parseArticle(final String fileName, String fileContent) { + fileContent = StringUtils.trim(fileContent); + String frontMatter = StringUtils.substringBefore(fileContent, "---"); + if (StringUtils.isBlank(frontMatter)) { + fileContent = StringUtils.substringAfter(fileContent, "---"); + frontMatter = StringUtils.substringBefore(fileContent, "---"); + } + + final JSONObject ret = new JSONObject(); + final Yaml yaml = new Yaml(); + Map elems; + + try { + elems = (Map) yaml.load(frontMatter); + } catch (final Exception e) { + // treat it as plain markdown + ret.put(Article.ARTICLE_TITLE, StringUtils.substringBeforeLast(fileName, ".")); + ret.put(Article.ARTICLE_CONTENT, fileContent); + ret.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(fileContent)); + ret.put(Article.ARTICLE_TAGS_REF, DEFAULT_TAG); + ret.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + ret.put(Article.ARTICLE_COMMENTABLE, true); + ret.put(Article.ARTICLE_VIEW_PWD, ""); + + return ret; + } + + String title = (String) elems.get("title"); + if (StringUtils.isBlank(title)) { + title = StringUtils.substringBeforeLast(fileName, "."); + } + ret.put(Article.ARTICLE_TITLE, title); + + String content = StringUtils.substringAfter(fileContent, frontMatter); + if (StringUtils.startsWith(content, "---")) { + content = StringUtils.substringAfter(content, "---"); + content = StringUtils.trim(content); + } + ret.put(Article.ARTICLE_CONTENT, content); + + final String abs = parseAbstract(elems, content); + ret.put(Article.ARTICLE_ABSTRACT, abs); + + final Date date = parseDate(elems); + ret.put(Article.ARTICLE_CREATED, date.getTime()); + + final String permalink = (String) elems.get("permalink"); + if (StringUtils.isNotBlank(permalink)) { + ret.put(Article.ARTICLE_PERMALINK, permalink); + } + + final List tags = parseTags(elems); + final StringBuilder tagBuilder = new StringBuilder(); + for (final String tag : tags) { + tagBuilder.append(tag).append(","); + } + tagBuilder.deleteCharAt(tagBuilder.length() - 1); + ret.put(Article.ARTICLE_TAGS_REF, tagBuilder.toString()); + + ret.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + ret.put(Article.ARTICLE_COMMENTABLE, true); + ret.put(Article.ARTICLE_VIEW_PWD, ""); + + return ret; + } + + private String parseAbstract(final Map map, final String content) { + String ret = (String) map.get("description"); + if (null == ret) { + ret = (String) map.get("summary"); + } + if (null == ret) { + ret = (String) map.get("abstract"); + } + if (StringUtils.isNotBlank(ret)) { + return ret; + } + + return Article.getAbstractText(content); + } + + private Date parseDate(final Map map) { + Object date = map.get("date"); + if (null == date) { + return new Date(); + } + + if (date instanceof String) { + try { + return DateUtils.parseDate((String) date, new String[]{ + "yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss", "dd/MM/yyyy HH:mm:ss", + "dd-MM-yyyy HH:mm:ss", "yyyyMMdd HH:mm:ss", + "yyyy/MM/dd HH:mm", "yyyy-MM-dd HH:mm", "dd/MM/yyyy HH:mm", + "dd-MM-yyyy HH:mm", "yyyyMMdd HH:mm"}); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Parse date [" + date + "] failed", e); + + throw new RuntimeException(e); + } + } else if (date instanceof Date) { + return (Date) date; + } + + return new Date(); + } + + private List parseTags(final Map map) { + final List ret = new ArrayList<>(); + + Object tags = map.get("tags"); + if (null == tags) { + tags = map.get("category"); + } + if (null == tags) { + tags = map.get("categories"); + } + if (null == tags) { + tags = map.get("keyword"); + } + if (null == tags) { + tags = map.get("keywords"); + } + if (null == tags) { + ret.add(DEFAULT_TAG); + + return ret; + } + + if (tags instanceof String) { + final String[] tagArr = ((String) tags).split(" "); + tags = Arrays.asList(tagArr); + } + final TreeSet tagSet = new TreeSet(); + for (final String tag : (List) tags) { + if (StringUtils.isBlank(tag)) { + tagSet.add(DEFAULT_TAG); + } else { + tagSet.add(tag); + } + } + ret.addAll(tagSet); + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/service/InitService.java b/src/main/java/org/b3log/solo/service/InitService.java new file mode 100644 index 00000000..c33e7eac --- /dev/null +++ b/src/main/java/org/b3log/solo/service/InitService.java @@ -0,0 +1,679 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.time.DateFormatUtils; +import org.apache.commons.lang.time.DateUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.plugin.PluginManager; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.latke.repository.jdbc.util.JdbcRepositories; +import org.b3log.latke.repository.jdbc.util.JdbcRepositories.CreateTableResult; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Ids; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.*; +import org.b3log.solo.model.Option.DefaultPreference; +import org.b3log.solo.repository.*; +import org.b3log.solo.util.Images; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.sql.Connection; +import java.text.ParseException; +import java.util.List; + +/** + * Solo initialization service. + * + * @author Liang Ding + * @version 1.5.2.34, Aug 18, 2019 + * @since 0.4.0 + */ +@Service +public class InitService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(InitService.class); + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Archive date repository. + */ + @Inject + private ArchiveDateRepository archiveDateRepository; + + /** + * Archive date-Article repository. + */ + @Inject + private ArchiveDateArticleRepository archiveDateArticleRepository; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Link repository. + */ + @Inject + private LinkRepository linkRepository; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Plugin manager. + */ + @Inject + private PluginManager pluginManager; + + /** + * Flag of init status. + */ + private static boolean inited; + + /** + * Flag of printed init prompt. + */ + private static boolean printedInitMsg; + + /** + * Determines Solo had been initialized. + * + * @return {@code true} if it had been initialized, {@code false} otherwise + */ + public boolean isInited() { + if (inited) { + return true; + } + + try { + inited = null != optionRepository.get(Option.ID_C_VERSION); + if (!inited && !printedInitMsg) { + LOGGER.log(Level.WARN, "Solo has not been initialized, please open your browser to init Solo"); + printedInitMsg = true; + } + + return inited; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Check init failed", e); + + System.exit(-1); + return false; + } + } + + /** + * Initializes database tables. + */ + public void initTables() { + try (final Connection chk = Connections.getConnection()) { + final String tablePrefix = Latkes.getLocalProperty("jdbc.tablePrefix") + "_"; + final boolean userTableExist = JdbcRepositories.existTable(tablePrefix + User.USER); + final boolean optionTableExist = JdbcRepositories.existTable(tablePrefix + Option.OPTION); + if (userTableExist && optionTableExist) { + return; + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Check tables failed, please make sure database existed and database configuration [jdbc.*] in local.props is correct [msg=" + e.getMessage() + "]"); + + System.exit(-1); + } + + LOGGER.info("It's your first time setup Solo, initialize tables in database [" + Latkes.getRuntimeDatabase() + "]"); + + if (Latkes.RuntimeDatabase.H2 == Latkes.getRuntimeDatabase()) { + String dataDir = Latkes.getLocalProperty("jdbc.URL"); + dataDir = dataDir.replace("~", System.getProperty("user.home")); + LOGGER.log(Level.INFO, "Your DATA will be stored in directory [" + dataDir + "], " + + "please pay more attention on it!"); + } + + final List createTableResults = JdbcRepositories.initAllTables(); + for (final CreateTableResult createTableResult : createTableResults) { + LOGGER.log(Level.DEBUG, "Creates table result [tableName={0}, isSuccess={1}]", + createTableResult.getName(), createTableResult.isSuccess()); + } + } + + /** + * Initializes Solo. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "userName": "", + * "userAvatar": "", // optional + * "userB3Key": "", // optional + * "userGitHubId": "" // optional + * } + */ + public void init(final JSONObject requestJSONObject) { + if (isInited()) { + return; + } + + final Transaction transaction = userRepository.beginTransaction(); + try { + initStatistic(); + initOptions(requestJSONObject); + initAdmin(requestJSONObject); + initLink(); + helloWorld(); + + transaction.commit(); + } catch (final Throwable e) { + LOGGER.log(Level.ERROR, "Initializes Solo failed", e); + + System.exit(-1); + } finally { + if (transaction.isActive()) { + transaction.rollback(); + } + } + + pluginManager.load(); + } + + /** + * Publishes the first article "Hello World" and the first comment with the specified locale. + * + * @throws Exception exception + */ + private void helloWorld() throws Exception { + final JSONObject article = new JSONObject(); + + article.put(Article.ARTICLE_TITLE, langPropsService.get("helloWorld.title")); + final String content = "![](" + Images.imageSize(Images.randImage(), Article.ARTICLE_THUMB_IMG_WIDTH, Article.ARTICLE_THUMB_IMG_HEIGHT) + ") \n\n" + + langPropsService.get("helloWorld.content"); + + article.put(Article.ARTICLE_ABSTRACT_TEXT, Article.getAbstractText(content)); + article.put(Article.ARTICLE_ABSTRACT, content); + article.put(Article.ARTICLE_CONTENT, content); + article.put(Article.ARTICLE_TAGS_REF, "Solo"); + article.put(Article.ARTICLE_PERMALINK, "/hello-solo"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENT_COUNT, 1); + article.put(Article.ARTICLE_VIEW_COUNT, 0); + final JSONObject admin = userRepository.getAdmin(); + final long now = System.currentTimeMillis(); + article.put(Article.ARTICLE_CREATED, now); + article.put(Article.ARTICLE_UPDATED, now); + article.put(Article.ARTICLE_PUT_TOP, false); + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + article.put(Article.ARTICLE_AUTHOR_ID, admin.optString(Keys.OBJECT_ID)); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + final String articleImg1URL = Article.getArticleImg1URL(article); + article.put(Article.ARTICLE_IMG1_URL, articleImg1URL); + + final String articleId = addHelloWorldArticle(article); + + final JSONObject comment = new JSONObject(); + comment.put(Keys.OBJECT_ID, articleId); + comment.put(Comment.COMMENT_NAME, "88250"); + comment.put(Comment.COMMENT_URL, "https://hacpai.com/member/88250"); + comment.put(Comment.COMMENT_CONTENT, langPropsService.get("helloWorld.comment.content")); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, ""); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, ""); + comment.put(Comment.COMMENT_THUMBNAIL_URL, "https://img.hacpai.com/avatar/1353745196354_1535379434567.png?imageView2/1/w/64/h/64/q/100"); + comment.put(Comment.COMMENT_CREATED, now); + comment.put(Comment.COMMENT_ON_ID, articleId); + final String commentId = Ids.genTimeMillisId(); + comment.put(Keys.OBJECT_ID, commentId); + final String commentSharpURL = Comment.getCommentSharpURLForArticle(article, commentId); + comment.put(Comment.COMMENT_SHARP_URL, commentSharpURL); + + commentRepository.add(comment); + + LOGGER.info("Hello World!"); + } + + /** + * Adds the specified "Hello World" article. + * + * @param article the specified "Hello World" article + * @return generated article id + * @throws RepositoryException repository exception + */ + private String addHelloWorldArticle(final JSONObject article) throws RepositoryException { + final String ret = Ids.genTimeMillisId(); + + try { + article.put(Keys.OBJECT_ID, ret); + + final String tagsString = article.optString(Article.ARTICLE_TAGS_REF); + final String[] tagTitles = tagsString.split(","); + final JSONArray tags = tag(tagTitles, article); + addTagArticleRelation(tags, article); + archiveDate(article); + articleRepository.add(article); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Adds an article failed", e); + + throw new RepositoryException(e); + } + + return ret; + } + + /** + * Archive the create date with the specified article. + * + * @param article the specified article, for example, + * { + * ...., + * "oId": "", + * "articleCreateDate": java.util.Date, + * .... + * } + * @throws RepositoryException repository exception + */ + public void archiveDate(final JSONObject article) throws RepositoryException { + final long created = article.optLong(Article.ARTICLE_CREATED); + final String createDateString = DateFormatUtils.format(created, "yyyy/MM"); + final JSONObject archiveDate = new JSONObject(); + + try { + archiveDate.put(ArchiveDate.ARCHIVE_TIME, DateUtils.parseDate(createDateString, new String[]{"yyyy/MM"}).getTime()); + archiveDateRepository.add(archiveDate); + } catch (final ParseException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + throw new RepositoryException(e); + } + + final JSONObject archiveDateArticleRelation = new JSONObject(); + archiveDateArticleRelation.put(ArchiveDate.ARCHIVE_DATE + "_" + Keys.OBJECT_ID, archiveDate.optString(Keys.OBJECT_ID)); + archiveDateArticleRelation.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, article.optString(Keys.OBJECT_ID)); + archiveDateArticleRepository.add(archiveDateArticleRelation); + } + + /** + * Adds relation of the specified tags and article. + * + * @param tags the specified tags + * @param article the specified article + * @throws RepositoryException repository exception + */ + private void addTagArticleRelation(final JSONArray tags, final JSONObject article) throws RepositoryException { + for (int i = 0; i < tags.length(); i++) { + final JSONObject tag = tags.optJSONObject(i); + final JSONObject tagArticleRelation = new JSONObject(); + tagArticleRelation.put(Tag.TAG + "_" + Keys.OBJECT_ID, tag.optString(Keys.OBJECT_ID)); + tagArticleRelation.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, article.optString(Keys.OBJECT_ID)); + tagArticleRepository.add(tagArticleRelation); + } + } + + /** + * Tags the specified article with the specified tag titles. + * + * @param tagTitles the specified tag titles + * @param article the specified article + * @return an array of tags + * @throws RepositoryException repository exception + */ + private JSONArray tag(final String[] tagTitles, final JSONObject article) throws RepositoryException { + final JSONArray ret = new JSONArray(); + + for (String tagTitle1 : tagTitles) { + final String tagTitle = tagTitle1.trim(); + final JSONObject tag = new JSONObject(); + + LOGGER.log(Level.TRACE, "Found a new tag[title={0}] in article[title={1}]", tagTitle, article.optString(Article.ARTICLE_TITLE)); + tag.put(Tag.TAG_TITLE, tagTitle); + final String tagId = tagRepository.add(tag); + tag.put(Keys.OBJECT_ID, tagId); + ret.put(tag); + } + + return ret; + } + + /** + * Initializes administrator with the specified request json object, and then logins it. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "userName": "", + * "userAvatar": "", // optional + * "userB3Key": "", // optional + * "userGitHubId": "" // optional + * } + * @throws Exception exception + */ + private void initAdmin(final JSONObject requestJSONObject) throws Exception { + LOGGER.debug("Initializing admin...."); + final JSONObject admin = new JSONObject(); + + admin.put(User.USER_NAME, requestJSONObject.getString(User.USER_NAME)); + admin.put(User.USER_URL, Latkes.getServePath()); + admin.put(User.USER_ROLE, Role.ADMIN_ROLE); + admin.put(UserExt.USER_AVATAR, requestJSONObject.optString(UserExt.USER_AVATAR)); + admin.put(UserExt.USER_B3_KEY, requestJSONObject.optString(UserExt.USER_B3_KEY)); + admin.put(UserExt.USER_GITHUB_ID, requestJSONObject.optString(UserExt.USER_GITHUB_ID)); + userRepository.add(admin); + + LOGGER.info("Initialized admin"); + } + + /** + * Initializes link. + * + * @throws Exception exception + */ + private void initLink() throws Exception { + LOGGER.debug("Initializing link...."); + final JSONObject link = new JSONObject(); + + link.put(Link.LINK_TITLE, "黑客派"); + link.put(Link.LINK_ADDRESS, "https://hacpai.com"); + link.put(Link.LINK_DESCRIPTION, "黑客与画家的社区"); + final int maxOrder = linkRepository.getMaxOrder(); + link.put(Link.LINK_ORDER, maxOrder + 1); + linkRepository.add(link); + LOGGER.info("Initialized link"); + } + + /** + * Initializes statistic. + * + * @throws RepositoryException repository exception + * @throws JSONException json exception + */ + private void initStatistic() throws RepositoryException, JSONException { + LOGGER.debug("Initializing statistic...."); + + final JSONObject statisticBlogViewCountOpt = new JSONObject(); + statisticBlogViewCountOpt.put(Keys.OBJECT_ID, Option.ID_C_STATISTIC_BLOG_VIEW_COUNT); + statisticBlogViewCountOpt.put(Option.OPTION_VALUE, "0"); + statisticBlogViewCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_STATISTIC); + optionRepository.add(statisticBlogViewCountOpt); + + LOGGER.info("Initialized statistic"); + } + + /** + * Initializes options. + * + * @param requestJSONObject the specified json object + * @throws Exception exception + */ + private void initOptions(final JSONObject requestJSONObject) throws Exception { + LOGGER.debug("Initializing preference...."); + + final JSONObject hljsThemeOpt = new JSONObject(); + hljsThemeOpt.put(Keys.OBJECT_ID, Option.ID_C_HLJS_THEME); + hljsThemeOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + hljsThemeOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_HLJS_THEME); + optionRepository.add(hljsThemeOpt); + + final JSONObject syncGitHubOpt = new JSONObject(); + syncGitHubOpt.put(Keys.OBJECT_ID, Option.ID_C_SYNC_GITHUB); + syncGitHubOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + syncGitHubOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_SYNC_GITHUB); + optionRepository.add(syncGitHubOpt); + + final JSONObject pullGitHubOpt = new JSONObject(); + pullGitHubOpt.put(Keys.OBJECT_ID, Option.ID_C_PULL_GITHUB); + pullGitHubOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + pullGitHubOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_PULL_GITHUB); + optionRepository.add(pullGitHubOpt); + + final JSONObject faviconURLOpt = new JSONObject(); + faviconURLOpt.put(Keys.OBJECT_ID, Option.ID_C_FAVICON_URL); + faviconURLOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + faviconURLOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_FAVICON_URL); + optionRepository.add(faviconURLOpt); + + final JSONObject customVarsOpt = new JSONObject(); + customVarsOpt.put(Keys.OBJECT_ID, Option.ID_C_CUSTOM_VARS); + customVarsOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + customVarsOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_CUSTOM_VARS); + optionRepository.add(customVarsOpt); + + final JSONObject noticeBoardOpt = new JSONObject(); + noticeBoardOpt.put(Keys.OBJECT_ID, Option.ID_C_NOTICE_BOARD); + noticeBoardOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + noticeBoardOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_NOTICE_BOARD); + optionRepository.add(noticeBoardOpt); + + final JSONObject metaDescriptionOpt = new JSONObject(); + metaDescriptionOpt.put(Keys.OBJECT_ID, Option.ID_C_META_DESCRIPTION); + metaDescriptionOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + metaDescriptionOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_META_DESCRIPTION); + optionRepository.add(metaDescriptionOpt); + + final JSONObject metaKeywordsOpt = new JSONObject(); + metaKeywordsOpt.put(Keys.OBJECT_ID, Option.ID_C_META_KEYWORDS); + metaKeywordsOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + metaKeywordsOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_META_KEYWORDS); + optionRepository.add(metaKeywordsOpt); + + final JSONObject htmlHeadOpt = new JSONObject(); + htmlHeadOpt.put(Keys.OBJECT_ID, Option.ID_C_HTML_HEAD); + htmlHeadOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + htmlHeadOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_HTML_HEAD); + optionRepository.add(htmlHeadOpt); + + final JSONObject relevantArticlesDisplayCountOpt = new JSONObject(); + relevantArticlesDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT); + relevantArticlesDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + relevantArticlesDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_RELEVANT_ARTICLES_DISPLAY_COUNT); + optionRepository.add(relevantArticlesDisplayCountOpt); + + final JSONObject randomArticlesDisplayCountOpt = new JSONObject(); + randomArticlesDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT); + randomArticlesDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + randomArticlesDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_RANDOM_ARTICLES_DISPLAY_COUNT); + optionRepository.add(randomArticlesDisplayCountOpt); + + final JSONObject externalRelevantArticlesDisplayCountOpt = new JSONObject(); + externalRelevantArticlesDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT); + externalRelevantArticlesDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + externalRelevantArticlesDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_COUNT); + optionRepository.add(externalRelevantArticlesDisplayCountOpt); + + final JSONObject mostViewArticleDisplayCountOpt = new JSONObject(); + mostViewArticleDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT); + mostViewArticleDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + mostViewArticleDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_MOST_VIEW_ARTICLES_DISPLAY_COUNT); + optionRepository.add(mostViewArticleDisplayCountOpt); + + final JSONObject articleListDisplayCountOpt = new JSONObject(); + articleListDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + articleListDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + articleListDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_ARTICLE_LIST_DISPLAY_COUNT); + optionRepository.add(articleListDisplayCountOpt); + + final JSONObject articleListPaginationWindowSizeOpt = new JSONObject(); + articleListPaginationWindowSizeOpt.put(Keys.OBJECT_ID, Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + articleListPaginationWindowSizeOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + articleListPaginationWindowSizeOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + optionRepository.add(articleListPaginationWindowSizeOpt); + + final JSONObject mostUsedTagDisplayCountOpt = new JSONObject(); + mostUsedTagDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_MOST_USED_TAG_DISPLAY_CNT); + mostUsedTagDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + mostUsedTagDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_MOST_USED_TAG_DISPLAY_COUNT); + optionRepository.add(mostUsedTagDisplayCountOpt); + + final JSONObject mostCommentArticleDisplayCountOpt = new JSONObject(); + mostCommentArticleDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT); + mostCommentArticleDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + mostCommentArticleDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_MOST_COMMENT_ARTICLE_DISPLAY_COUNT); + optionRepository.add(mostCommentArticleDisplayCountOpt); + + final JSONObject recentArticleDisplayCountOpt = new JSONObject(); + recentArticleDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_RECENT_ARTICLE_DISPLAY_CNT); + recentArticleDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + recentArticleDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_RECENT_ARTICLE_DISPLAY_COUNT); + optionRepository.add(recentArticleDisplayCountOpt); + + final JSONObject recentCommentDisplayCountOpt = new JSONObject(); + recentCommentDisplayCountOpt.put(Keys.OBJECT_ID, Option.ID_C_RECENT_COMMENT_DISPLAY_CNT); + recentCommentDisplayCountOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + recentCommentDisplayCountOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_RECENT_COMMENT_DISPLAY_COUNT); + optionRepository.add(recentCommentDisplayCountOpt); + + final JSONObject blogTitleOpt = new JSONObject(); + blogTitleOpt.put(Keys.OBJECT_ID, Option.ID_C_BLOG_TITLE); + blogTitleOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + blogTitleOpt.put(Option.OPTION_VALUE, requestJSONObject.optString(User.USER_NAME) + " 的个人博客"); + optionRepository.add(blogTitleOpt); + + final JSONObject blogSubtitleOpt = new JSONObject(); + blogSubtitleOpt.put(Keys.OBJECT_ID, Option.ID_C_BLOG_SUBTITLE); + blogSubtitleOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + blogSubtitleOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_BLOG_SUBTITLE); + optionRepository.add(blogSubtitleOpt); + + final JSONObject localeStringOpt = new JSONObject(); + localeStringOpt.put(Keys.OBJECT_ID, Option.ID_C_LOCALE_STRING); + localeStringOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + localeStringOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_LANGUAGE); + optionRepository.add(localeStringOpt); + + final JSONObject enableArticleUpdateHintOpt = new JSONObject(); + enableArticleUpdateHintOpt.put(Keys.OBJECT_ID, Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT); + enableArticleUpdateHintOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + enableArticleUpdateHintOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_ENABLE_ARTICLE_UPDATE_HINT); + optionRepository.add(enableArticleUpdateHintOpt); + + final JSONObject signsOpt = new JSONObject(); + signsOpt.put(Keys.OBJECT_ID, Option.ID_C_SIGNS); + signsOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + signsOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_SIGNS); + optionRepository.add(signsOpt); + + final JSONObject timeZoneIdOpt = new JSONObject(); + timeZoneIdOpt.put(Keys.OBJECT_ID, Option.ID_C_TIME_ZONE_ID); + timeZoneIdOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + timeZoneIdOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_TIME_ZONE); + optionRepository.add(timeZoneIdOpt); + + final JSONObject allowVisitDraftViaPermalinkOpt = new JSONObject(); + allowVisitDraftViaPermalinkOpt.put(Keys.OBJECT_ID, Option.ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK); + allowVisitDraftViaPermalinkOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + allowVisitDraftViaPermalinkOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_ALLOW_VISIT_DRAFT_VIA_PERMALINK); + optionRepository.add(allowVisitDraftViaPermalinkOpt); + + final JSONObject commentableOpt = new JSONObject(); + commentableOpt.put(Keys.OBJECT_ID, Option.ID_C_COMMENTABLE); + commentableOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + commentableOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_COMMENTABLE); + optionRepository.add(commentableOpt); + + final JSONObject versionOpt = new JSONObject(); + versionOpt.put(Keys.OBJECT_ID, Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + versionOpt.put(Option.OPTION_VALUE, SoloServletListener.VERSION); + optionRepository.add(versionOpt); + + final JSONObject articleListStyleOpt = new JSONObject(); + articleListStyleOpt.put(Keys.OBJECT_ID, Option.ID_C_ARTICLE_LIST_STYLE); + articleListStyleOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + articleListStyleOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_ARTICLE_LIST_STYLE); + optionRepository.add(articleListStyleOpt); + + final JSONObject feedOutputModeOpt = new JSONObject(); + feedOutputModeOpt.put(Keys.OBJECT_ID, Option.ID_C_FEED_OUTPUT_MODE); + feedOutputModeOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + feedOutputModeOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_FEED_OUTPUT_MODE); + optionRepository.add(feedOutputModeOpt); + + final JSONObject feedOutputCntOpt = new JSONObject(); + feedOutputCntOpt.put(Keys.OBJECT_ID, Option.ID_C_FEED_OUTPUT_CNT); + feedOutputCntOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + feedOutputCntOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_FEED_OUTPUT_CNT); + optionRepository.add(feedOutputCntOpt); + + final JSONObject footerContentOpt = new JSONObject(); + footerContentOpt.put(Keys.OBJECT_ID, Option.ID_C_FOOTER_CONTENT); + footerContentOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + footerContentOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_FOOTER_CONTENT); + optionRepository.add(footerContentOpt); + + final JSONObject skinDirNameOpt = new JSONObject(); + skinDirNameOpt.put(Keys.OBJECT_ID, Option.ID_C_SKIN_DIR_NAME); + skinDirNameOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_SKIN); + skinDirNameOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_SKIN_DIR_NAME); + optionRepository.add(skinDirNameOpt); + + final JSONObject mobileSkinDirNameOpt = new JSONObject(); + mobileSkinDirNameOpt.put(Keys.OBJECT_ID, Option.ID_C_MOBILE_SKIN_DIR_NAME); + mobileSkinDirNameOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_SKIN); + mobileSkinDirNameOpt.put(Option.OPTION_VALUE, DefaultPreference.DEFAULT_MOBILE_SKIN_DIR_NAME); + optionRepository.add(mobileSkinDirNameOpt); + + LOGGER.info("Initialized preference"); + } +} diff --git a/src/main/java/org/b3log/solo/service/LinkMgmtService.java b/src/main/java/org/b3log/solo/service/LinkMgmtService.java new file mode 100644 index 00000000..997b6e7d --- /dev/null +++ b/src/main/java/org/b3log/solo/service/LinkMgmtService.java @@ -0,0 +1,202 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Link; +import org.b3log.solo.repository.LinkRepository; +import org.json.JSONObject; + +/** + * Link management service. + * + * @author Liang Ding + * @version 1.0.0.1, Nov 2, 2011 + * @since 0.4.0 + */ +@Service +public class LinkMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(LinkMgmtService.class); + + /** + * Link repository. + */ + @Inject + private LinkRepository linkRepository; + + /** + * Removes a link specified by the given link id. + * + * @param linkId the given link id + * @throws ServiceException service exception + */ + public void removeLink(final String linkId) + throws ServiceException { + final Transaction transaction = linkRepository.beginTransaction(); + + try { + linkRepository.remove(linkId); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Removes a link[id=" + linkId + "] failed", e); + throw new ServiceException(e); + } + } + + /** + * Updates a link by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "link": { + * "oId": "", + * "linkTitle": "", + * "linkAddress": "" + * } + * see {@link Link} for more details + * @throws ServiceException service exception + */ + public void updateLink(final JSONObject requestJSONObject) + throws ServiceException { + final Transaction transaction = linkRepository.beginTransaction(); + + try { + final JSONObject link = requestJSONObject.getJSONObject(Link.LINK); + final String linkId = link.getString(Keys.OBJECT_ID); + final JSONObject oldLink = linkRepository.get(linkId); + + link.put(Link.LINK_ORDER, oldLink.getInt(Link.LINK_ORDER)); + + linkRepository.update(linkId, link); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, e.getMessage(), e); + + throw new ServiceException(e); + } + } + + /** + * Changes the order of a link specified by the given link id with the + * specified direction. + * + * @param linkId the given link id + * @param direction the specified direction, "up"/"down" + * @throws ServiceException service exception + */ + public void changeOrder(final String linkId, final String direction) + throws ServiceException { + final Transaction transaction = linkRepository.beginTransaction(); + + try { + final JSONObject srcLink = linkRepository.get(linkId); + final int srcLinkOrder = srcLink.getInt(Link.LINK_ORDER); + + JSONObject targetLink = null; + + if ("up".equals(direction)) { + targetLink = linkRepository.getUpper(linkId); + } else { // Down + targetLink = linkRepository.getUnder(linkId); + } + + if (null == targetLink) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.WARN, "Cant not find the target link of source link[order={0}]", srcLinkOrder); + return; + } + + // Swaps + srcLink.put(Link.LINK_ORDER, targetLink.getInt(Link.LINK_ORDER)); + targetLink.put(Link.LINK_ORDER, srcLinkOrder); + + linkRepository.update(srcLink.getString(Keys.OBJECT_ID), srcLink); + linkRepository.update(targetLink.getString(Keys.OBJECT_ID), targetLink); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Changes link's order failed", e); + + throw new ServiceException(e); + } + } + + /** + * Adds a link with the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "link": { + * "linkTitle": "", + * "linkAddress": "", + * "linkDescription": "" // optional + * } + * }, see {@link Link} for more details + * @return generated link id + * @throws ServiceException service exception + */ + public String addLink(final JSONObject requestJSONObject) + throws ServiceException { + final Transaction transaction = linkRepository.beginTransaction(); + + try { + final JSONObject link = requestJSONObject.getJSONObject(Link.LINK); + final int maxOrder = linkRepository.getMaxOrder(); + + link.put(Link.LINK_ORDER, maxOrder + 1); + final String ret = linkRepository.add(link); + + transaction.commit(); + + return ret; + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Adds a link failed", e); + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/LinkQueryService.java b/src/main/java/org/b3log/solo/service/LinkQueryService.java new file mode 100644 index 00000000..5012c197 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/LinkQueryService.java @@ -0,0 +1,150 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.SortDirection; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Paginator; +import org.b3log.solo.model.Link; +import org.b3log.solo.repository.LinkRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * Link query service. + * + * @author Liang Ding + * @version 1.0.0.2, Oct 31, 2011 + * @since 0.4.0 + */ +@Service +public class LinkQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(LinkQueryService.class); + + /** + * Link repository. + */ + @Inject + private LinkRepository linkRepository; + + /** + * Gets links by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10 + * see {@link Pagination} for more details + * @return for example, + *
+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "links": [{
+     *         "oId": "",
+     *         "linkTitle": "",
+     *         "linkAddress": "",
+     *         ""linkDescription": ""
+     *      }, ....]
+     * }
+     * 
+ * @throws ServiceException service exception + * @see Pagination + */ + public JSONObject getLinks(final JSONObject requestJSONObject) throws ServiceException { + final JSONObject ret = new JSONObject(); + + try { + final int currentPageNum = requestJSONObject.getInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.getInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.getInt(Pagination.PAGINATION_WINDOW_SIZE); + + final Query query = new Query().setPage(currentPageNum, pageSize).addSort(Link.LINK_ORDER, SortDirection.ASCENDING); + final JSONObject result = linkRepository.get(query); + final int pageCount = result.getJSONObject(Pagination.PAGINATION).getInt(Pagination.PAGINATION_PAGE_COUNT); + + final JSONObject pagination = new JSONObject(); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + final JSONArray links = result.getJSONArray(Keys.RESULTS); + + ret.put(Pagination.PAGINATION, pagination); + ret.put(Link.LINKS, links); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets links failed", e); + throw new ServiceException(e); + } + } + + /** + * Gets a link by the specified link id. + * + * @param linkId the specified link id + * @return for example, + *
+     * {
+     *     "link": {
+     *         "oId": "",
+     *         "linkTitle": "",
+     *         "linkAddress": "",
+     *         "linkDescription": ""
+     *     }
+     * }
+     * 
, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getLink(final String linkId) throws ServiceException { + final JSONObject ret = new JSONObject(); + + try { + final JSONObject link = linkRepository.get(linkId); + + if (null == link) { + return null; + } + + ret.put(Link.LINK, link); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets a link failed", e); + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/OptionMgmtService.java b/src/main/java/org/b3log/solo/service/OptionMgmtService.java new file mode 100644 index 00000000..1206b6bd --- /dev/null +++ b/src/main/java/org/b3log/solo/service/OptionMgmtService.java @@ -0,0 +1,107 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +/** + * Option management service. + * + * @author Liang Ding + * @version 1.0.0.0, Apr 16, 2013 + * @since 0.6.0 + */ +@Service +public class OptionMgmtService { + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * Adds or updates the specified option. + * + * @param option the specified option + * @return option id + * @throws ServiceException + */ + public String addOrUpdateOption(final JSONObject option) throws ServiceException { + final Transaction transaction = optionRepository.beginTransaction(); + + try { + String id = option.optString(Keys.OBJECT_ID); + + if (StringUtils.isBlank(id)) { + id = optionRepository.add(option); + } else { + final JSONObject old = optionRepository.get(id); + + if (null == old) { // The id is specified by caller + id = optionRepository.add(option); + } else { + old.put(Option.OPTION_CATEGORY, option.optString(Option.OPTION_CATEGORY)); + old.put(Option.OPTION_VALUE, option.optString(Option.OPTION_VALUE)); + + optionRepository.update(id, old); + } + } + + transaction.commit(); + + return id; + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e); + } + } + + /** + * Removes the option specified by the given option id. + * + * @param optionId the given option id + * @throws ServiceException service exception + */ + public void removeOption(final String optionId) throws ServiceException { + final Transaction transaction = optionRepository.beginTransaction(); + + try { + optionRepository.remove(optionId); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/OptionQueryService.java b/src/main/java/org/b3log/solo/service/OptionQueryService.java new file mode 100644 index 00000000..e10a82f1 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/OptionQueryService.java @@ -0,0 +1,133 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +/** + * Option query service. + * + * @author Liang Ding + * @version 1.0.0.4, Jun 13, 2019 + * @since 0.6.0 + */ +@Service +public class OptionQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(OptionQueryService.class); + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * Gets the skin. + * + * @return skin, returns {@code null} if not found + */ + public JSONObject getSkin() { + try { + return getOptions(Option.CATEGORY_C_SKIN); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets skin failed", e); + + return null; + } + } + + /** + * Gets the user preference. + * + * @return user preference, returns {@code null} if not found + */ + public JSONObject getPreference() { + try { + return getOptions(Option.CATEGORY_C_PREFERENCE); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets preference failed", e); + + return null; + } + } + + /** + * Checks whether allow comment globally. + * + * @return {@code true} to allow comment, returns {@code false} otherwise + */ + public boolean allowComment() { + try { + final JSONObject opt = optionRepository.get(Option.ID_C_COMMENTABLE); + + return opt.optBoolean(Option.OPTION_VALUE); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Checks allow comment failed", e); + + return false; + } + } + + /** + * Gets an option with the specified option id. + * + * @param optionId the specified option id + * @return an option, returns {@code null} if not found + */ + public JSONObject getOptionById(final String optionId) { + try { + return optionRepository.get(optionId); + } catch (final RepositoryException e) { + return null; + } + } + + /** + * Gets options with the specified category. + *

+ * All options with the specified category will be merged into one json object as the return value. + *

+ * + * @param category the specified category + * @return all options with the specified category, for example, + *
+     * {
+     *     "${optionId}": "${optionValue}",
+     *     ....
+     * }
+     * 
, returns {@code null} if not found + */ + public JSONObject getOptions(final String category) { + try { + return optionRepository.getOptions(category); + } catch (final Exception e) { + return null; + } + } +} diff --git a/src/main/java/org/b3log/solo/service/PageMgmtService.java b/src/main/java/org/b3log/solo/service/PageMgmtService.java new file mode 100644 index 00000000..8700a323 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/PageMgmtService.java @@ -0,0 +1,270 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Page; +import org.b3log.solo.repository.CommentRepository; +import org.b3log.solo.repository.PageRepository; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Page management service. + * + * @author Liang Ding + * @author Vanessa + * @version 1.1.0.19, Jun 6, 2019 + * @since 0.4.0 + */ +@Service +public class PageMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PageMgmtService.class); + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * User query service. + */ + @Inject + private UserQueryService userQueryService; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Permalink query service. + */ + @Inject + private PermalinkQueryService permalinkQueryService; + + /** + * Statistic management service. + */ + @Inject + private StatisticMgmtService statisticMgmtService; + + /** + * Statistic query service. + */ + @Inject + private StatisticQueryService statisticQueryService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Option management service. + */ + @Inject + private OptionMgmtService optionMgmtService; + + /** + * Updates a page by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "page": { + * "oId": "", + * "pageTitle": "", + * "pageOrder": int, + * "pagePermalink": "", + * "pageOpenTarget": "", + * "pageIcon": "" // optional + * } + * }, see {@link Page} for more details + * @throws ServiceException service exception + */ + public void updatePage(final JSONObject requestJSONObject) throws ServiceException { + final Transaction transaction = pageRepository.beginTransaction(); + try { + final JSONObject page = requestJSONObject.getJSONObject(Page.PAGE); + final String pageId = page.getString(Keys.OBJECT_ID); + final JSONObject oldPage = pageRepository.get(pageId); + final JSONObject newPage = new JSONObject(page, JSONObject.getNames(page)); + newPage.put(Page.PAGE_ORDER, oldPage.getInt(Page.PAGE_ORDER)); + final String permalink = page.optString(Page.PAGE_PERMALINK).trim(); + newPage.put(Page.PAGE_PERMALINK, permalink); + page.put(Page.PAGE_ICON, page.optString(Page.PAGE_ICON)); + + pageRepository.update(pageId, newPage); + transaction.commit(); + + LOGGER.log(Level.DEBUG, "Updated a page[id={0}]", pageId); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e); + } + } + + /** + * Removes a page specified by the given page id. + * + * @param pageId the given page id + * @throws ServiceException service exception + */ + public void removePage(final String pageId) throws ServiceException { + final Transaction transaction = pageRepository.beginTransaction(); + try { + pageRepository.remove(pageId); + commentRepository.removeComments(pageId); + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Removes a page[id=" + pageId + "] failed", e); + + throw new ServiceException(e); + } + } + + /** + * Adds a page with the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * { + * "page": { + * "pageTitle": "", + * "pageOpenTarget": "", + * "pagePermalink": "", + * "pageIcon": "" // optional + * } + * }, see {@link Page} for more details + * @return generated page id + * @throws ServiceException if permalink format checks failed or persists failed + */ + public String addPage(final JSONObject requestJSONObject) throws ServiceException { + final Transaction transaction = pageRepository.beginTransaction(); + + try { + final JSONObject page = requestJSONObject.getJSONObject(Page.PAGE); + final int maxOrder = pageRepository.getMaxOrder(); + page.put(Page.PAGE_ORDER, maxOrder + 1); + + final String permalink = page.optString(Page.PAGE_PERMALINK); + if (permalinkQueryService.exist(permalink)) { + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(langPropsService.get("duplicatedPermalinkLabel")); + } + + page.put(Page.PAGE_PERMALINK, permalink); + page.put(Page.PAGE_ICON, page.optString(Page.PAGE_ICON)); + final String ret = pageRepository.add(page); + transaction.commit(); + + return ret; + } catch (final JSONException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(e); + } + } + + /** + * Changes the order of a page specified by the given page id with the specified direction. + * + * @param pageId the given page id + * @param direction the specified direction, "up"/"down" + * @throws ServiceException service exception + */ + public void changeOrder(final String pageId, final String direction) throws ServiceException { + final Transaction transaction = pageRepository.beginTransaction(); + try { + final JSONObject srcPage = pageRepository.get(pageId); + final int srcPageOrder = srcPage.getInt(Page.PAGE_ORDER); + + JSONObject targetPage; + if ("up".equals(direction)) { + targetPage = pageRepository.getUpper(pageId); + } else { // Down + targetPage = pageRepository.getUnder(pageId); + } + + if (null == targetPage) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.WARN, "Cant not find the target page of source page[order={0}]", srcPageOrder); + return; + } + + // Swaps + srcPage.put(Page.PAGE_ORDER, targetPage.getInt(Page.PAGE_ORDER)); + targetPage.put(Page.PAGE_ORDER, srcPageOrder); + pageRepository.update(srcPage.getString(Keys.OBJECT_ID), srcPage, Page.PAGE_ORDER); + pageRepository.update(targetPage.getString(Keys.OBJECT_ID), targetPage, Page.PAGE_ORDER); + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Changes page's order failed", e); + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/PageQueryService.java b/src/main/java/org/b3log/solo/service/PageQueryService.java new file mode 100644 index 00000000..bf081679 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/PageQueryService.java @@ -0,0 +1,152 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.SortDirection; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Paginator; +import org.b3log.solo.model.Page; +import org.b3log.solo.repository.PageRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * Page query service. + * + * @author Liang Ding + * @version 1.0.0.1, Apr 19, 2019 + * @since 0.4.0 + */ +@Service +public class PageQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PageQueryService.class); + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Gets a page by the specified page id. + * + * @param pageId the specified page id + * @return for example, + *
+     * {
+     *     "page": {
+     *         "oId": "",
+     *         "pageTitle": "",
+     *         "pageOrder": int,
+     *         "pagePermalink": "",
+     *         "pageOpenTarget": "",
+     *         "pageIcon": ""
+     *     }
+     * }
+     * 
, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getPage(final String pageId) throws ServiceException { + final JSONObject ret = new JSONObject(); + + try { + final JSONObject page = pageRepository.get(pageId); + if (null == page) { + return null; + } + + ret.put(Page.PAGE, page); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + + throw new ServiceException(e); + } + } + + /** + * Gets pages by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10 + * see {@link Pagination} for more details + * @return for example, + *
+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "pages": [{
+     *         "oId": "",
+     *         "pageTitle": "",
+     *         "pageOrder": int,
+     *         "pagePermalink": "",
+     *         "pageOpenTarget": ""
+     *      }, ....]
+     * }
+     * 
+ * @throws ServiceException service exception + * @see Pagination + */ + public JSONObject getPages(final JSONObject requestJSONObject) throws ServiceException { + final JSONObject ret = new JSONObject(); + + try { + final int currentPageNum = requestJSONObject.getInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.getInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.getInt(Pagination.PAGINATION_WINDOW_SIZE); + + final Query query = new Query().setPage(currentPageNum, pageSize).addSort(Page.PAGE_ORDER, SortDirection.ASCENDING).setPageCount(1); + final JSONObject result = pageRepository.get(query); + final int pageCount = result.getJSONObject(Pagination.PAGINATION).getInt(Pagination.PAGINATION_PAGE_COUNT); + + final JSONObject pagination = new JSONObject(); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + final JSONArray pages = result.getJSONArray(Keys.RESULTS); + ret.put(Pagination.PAGINATION, pagination); + ret.put(Page.PAGES, pages); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets pages failed", e); + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/PermalinkQueryService.java b/src/main/java/org/b3log/solo/service/PermalinkQueryService.java new file mode 100644 index 00000000..764a82e5 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/PermalinkQueryService.java @@ -0,0 +1,203 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Strings; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.PageRepository; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Permalink query service. + * + * @author Liang Ding + * @version 1.0.0.7, Apr 19, 2019 + * @since 0.6.1 + */ +@Service +public class PermalinkQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PermalinkQueryService.class); + + /** + * Page repository. + */ + @Inject + private PageRepository pageRepository; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Reserved permalinks. + */ + public static final String[] RESERVED_LINKS = new String[]{ + "/", "/article", "/tags.html", "/tags", "/page", "/atom.xml", "/rss.xml", "/articles/random", "/captcha", "/kill-browser", + "/article/comments", "/add-article-from-symphony-comment.do", "/page/comments", "/get-article-content", "/sitemap.xml", + "/logout", "/get-article-content", "/admin-index.do", "/admin-article.do", "/admin-article-list.do", + "/admin-link-list.do", "/admin-preference.do", "/admin-file-list.do", "/admin-page-list.do", "/admin-others.do", + "/admin-draft-list.do", "/admin-user-list.do", "/admin-plugin-list.do", "/admin-main.do", "/admin-about.do", "/admin-label", + "/admin-about.do", "/start" + }; + + /** + * Checks whether the specified article permalink matches the system generated format pattern ("/articles/yyyy/MM/dd/${articleId}.html"). + * + * @param permalink the specified permalink + * @return {@code true} if matches, returns {@code false} otherwise + */ + public static boolean matchDefaultArticlePermalinkFormat(final String permalink) { + final Pattern pattern = Pattern.compile("/articles/\\d{4}/\\d{2}/\\d{2}/\\d+\\.html"); + final Matcher matcher = pattern.matcher(permalink); + + return matcher.matches(); + } + + /** + * Checks whether the specified permalink is a {@link #invalidArticlePermalinkFormat(java.lang.String) invalid article + * permalink format} and {@link #invalidPagePermalinkFormat(java.lang.String) invalid page permalink format}. + * + * @param permalink the specified permalink + * @return {@code true} if invalid, returns {@code false} otherwise + */ + public static boolean invalidPermalinkFormat(final String permalink) { + return invalidArticlePermalinkFormat(permalink) && invalidPagePermalinkFormat(permalink); + } + + /** + * Checks whether the specified article permalink is invalid on format. + * + * @param permalink the specified article permalink + * @return {@code true} if invalid, returns {@code false} otherwise + */ + public static boolean invalidArticlePermalinkFormat(final String permalink) { + if (StringUtils.isBlank(permalink)) { + return true; + } + + if (matchDefaultArticlePermalinkFormat(permalink)) { + return false; + } + + return invalidUserDefinedPermalinkFormat(permalink); + } + + /** + * Checks whether the specified page permalink is invalid on format. + * + * @param permalink the specified page permalink + * @return {@code true} if invalid, returns {@code false} otherwise + */ + public static boolean invalidPagePermalinkFormat(final String permalink) { + if (StringUtils.isBlank(permalink)) { + return true; + } + + return invalidUserDefinedPermalinkFormat(permalink); + } + + /** + * Checks whether the specified user-defined permalink is invalid on format. + * + * @param permalink the specified user-defined permalink + * @return {@code true} if invalid, returns {@code false} otherwise + */ + private static boolean invalidUserDefinedPermalinkFormat(final String permalink) { + if (StringUtils.isBlank(permalink)) { + return true; + } + + if (isReservedLink(permalink)) { + return true; + } + + if (Strings.isNumeric(permalink.substring(1))) { + // Conflict with pagination + return true; + } + + int slashCnt = 0; + + for (int i = 0; i < permalink.length(); i++) { + if ('/' == permalink.charAt(i)) { + slashCnt++; + } + + if (slashCnt > 1) { + return true; + } + } + + return !Strings.isURL(Latkes.getServer() + permalink); + } + + /** + * Determines whether the specified request URI is a reserved link. + *

+ * A URI starts with one of {@link PermalinkQueryService#RESERVED_LINKS reserved links} + * will be treated as reserved link. + *

+ * + * @param requestURI the specified request URI + * @return {@code true} if it is a reserved link, returns {@code false} otherwise + */ + private static boolean isReservedLink(final String requestURI) { + for (int i = 0; i < RESERVED_LINKS.length; i++) { + final String reservedLink = RESERVED_LINKS[i]; + + if (reservedLink.startsWith(requestURI)) { + + return true; + } + } + + return false; + } + + /** + * Determines whether the specified permalink exists. + * + * @param permalink the specified permalink + * @return {@code true} if exists, returns {@code false} otherwise + */ + public boolean exist(final String permalink) { + try { + return isReservedLink(permalink) || null != articleRepository.getByPermalink(permalink) + || null != pageRepository.getByPermalink(permalink) || permalink.endsWith(".ftl"); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Determines whether the permalink[" + permalink + "] exists failed, returns true", e); + + return true; + } + } +} diff --git a/src/main/java/org/b3log/solo/service/PluginMgmtService.java b/src/main/java/org/b3log/solo/service/PluginMgmtService.java new file mode 100644 index 00000000..a2b68102 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/PluginMgmtService.java @@ -0,0 +1,253 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Plugin; +import org.b3log.latke.plugin.AbstractPlugin; +import org.b3log.latke.plugin.PluginManager; +import org.b3log.latke.plugin.PluginStatus; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.repository.PluginRepository; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.Map; + +/** + * Plugin management service. + * + * @author Liang Ding + * @version 1.0.0.1, Aug 27, 2018 + * @since 0.4.0 + */ +@Service +public class PluginMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PluginMgmtService.class); + + /** + * Plugin repository. + */ + @Inject + private PluginRepository pluginRepository; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Initialization service. + */ + @Inject + private InitService initService; + + /** + * Plugin manager. + */ + @Inject + private PluginManager pluginManager; + + /** + * Updates datastore plugin descriptions with the specified plugins. + * + * @param plugins the specified plugins + * @throws Exception exception + */ + public void refresh(final List plugins) throws Exception { + if (!initService.isInited()) { + return; + } + + final List persistedPlugins = pluginRepository.getList(new Query()); + try { + // Reads plugin status from datastore and clear plugin datastore + for (final JSONObject oldPluginDesc : persistedPlugins) { + final String descId = oldPluginDesc.getString(Keys.OBJECT_ID); + final AbstractPlugin plugin = get(plugins, descId); + + pluginRepository.remove(descId); + + if (null != plugin) { + final String status = oldPluginDesc.getString(Plugin.PLUGIN_STATUS); + final String setting = oldPluginDesc.optString(Plugin.PLUGIN_SETTING); + + plugin.setStatus(PluginStatus.valueOf(status)); + try { + if (StringUtils.isNotBlank(setting)) { + plugin.setSetting(new JSONObject(setting)); + } + } catch (final JSONException e) { + LOGGER.log(Level.WARN, "the formatter of the old config failed to convert to json", e); + } + } + } + + // Adds these plugins into datastore + for (final AbstractPlugin plugin : plugins) { + final JSONObject pluginDesc = plugin.toJSONObject(); + + pluginRepository.add(pluginDesc); + + LOGGER.log(Level.TRACE, "Refreshed plugin[{0}]", pluginDesc); + } + + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Refresh plugins failed", e); + } + } + + /** + * Gets a plugin in the specified plugins with the specified id. + * + * @param plugins the specified plugins + * @param id the specified id, must NOT be {@code null} + * @return a plugin, returns {@code null} if not found + */ + private AbstractPlugin get(final List plugins, final String id) { + if (null == id) { + throw new IllegalArgumentException("id must not be null"); + } + + for (final AbstractPlugin plugin : plugins) { + if (id.equals(plugin.getId())) { + return plugin; + } + } + + return null; + } + + /** + * Sets a plugin's status with the specified plugin id, status. + * + * @param pluginId the specified plugin id + * @param status the specified status, see {@link PluginStatus} + * @return for example, + *
+     * {
+     *     "sc": boolean,
+     *     "msg": ""
+     * }
+     * 
+ */ + public JSONObject setPluginStatus(final String pluginId, final String status) { + final Map langs = langPropsService.getAll(Latkes.getLocale()); + + final List plugins = pluginManager.getPlugins(); + + final JSONObject ret = new JSONObject(); + + for (final AbstractPlugin plugin : plugins) { + if (plugin.getId().equals(pluginId)) { + final Transaction transaction = pluginRepository.beginTransaction(); + + try { + plugin.setStatus(PluginStatus.valueOf(status)); + pluginRepository.update(pluginId, plugin.toJSONObject()); + + transaction.commit(); + + plugin.changeStatus(); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langs.get("setSuccLabel")); + + return ret; + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Set plugin status error", e); + + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langs.get("setFailLabel")); + + return ret; + } + } + } + + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langs.get("refreshAndRetryLabel")); + + return ret; + } + + /** + * updatePluginSetting. + * + * @param pluginId the specified pluginoId + * @param setting the specified setting + * @return the ret json + */ + public JSONObject updatePluginSetting(final String pluginId, final String setting) { + final JSONObject ret = new JSONObject(); + + final Map langs = langPropsService.getAll(Latkes.getLocale()); + final List plugins = pluginManager.getPlugins(); + for (final AbstractPlugin plugin : plugins) { + if (plugin.getId().equals(pluginId)) { + final Transaction transaction = pluginRepository.beginTransaction(); + + try { + final JSONObject pluginJson = plugin.toJSONObject(); + pluginJson.put(Plugin.PLUGIN_SETTING, setting); + pluginRepository.update(pluginId, pluginJson); + + transaction.commit(); + + ret.put(Keys.STATUS_CODE, true); + ret.put(Keys.MSG, langs.get("setSuccLabel")); + + return ret; + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + LOGGER.log(Level.ERROR, "Set plugin status error", e); + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langs.get("setFailLabel")); + + return ret; + } + } + } + + ret.put(Keys.STATUS_CODE, false); + ret.put(Keys.MSG, langs.get("refreshAndRetryLabel")); + + return ret; + + } +} diff --git a/src/main/java/org/b3log/solo/service/PluginQueryService.java b/src/main/java/org/b3log/solo/service/PluginQueryService.java new file mode 100644 index 00000000..6b48ebd7 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/PluginQueryService.java @@ -0,0 +1,159 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.Plugin; +import org.b3log.latke.plugin.AbstractPlugin; +import org.b3log.latke.plugin.PluginManager; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Paginator; +import org.b3log.solo.repository.PluginRepository; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Plugin query service. + * + * @author Liang Ding + * @version 1.0.0.0, Oct 27, 2011 + * @since 0.4.0 + */ +@Service +public class PluginQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PluginQueryService.class); + + /** + * Plugin repository. + */ + @Inject + private PluginRepository pluginRepository; + + /** + * Plugin manager. + */ + @Inject + private PluginManager pluginManager; + + /** + * Gets plugins by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10, + * see {@link Pagination} for more details + * @return for example, + *
+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "plugins": [{
+     *         "name": "",
+     *         "version": "",
+     *         "author": "",
+     *         "status": "", // Enumeration name of {@link org.b3log.latke.plugin.PluginStatus}
+     *      }, ....]
+     * }
+     * 
+ * @throws ServiceException service exception + * @see Pagination + */ + public JSONObject getPlugins(final JSONObject requestJSONObject) + throws ServiceException { + final JSONObject ret = new JSONObject(); + + try { + final int currentPageNum = requestJSONObject.getInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.getInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.getInt(Pagination.PAGINATION_WINDOW_SIZE); + + final List pluginJSONObjects = new ArrayList(); + final List plugins = pluginManager.getPlugins(); + + for (final AbstractPlugin plugin : plugins) { + final JSONObject jsonObject = plugin.toJSONObject(); + + pluginJSONObjects.add(jsonObject); + } + + final int pageCount = (int) Math.ceil((double) pluginJSONObjects.size() / (double) pageSize); + final JSONObject pagination = new JSONObject(); + + ret.put(Pagination.PAGINATION, pagination); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + + final int start = pageSize * (currentPageNum - 1); + int end = start + pageSize; + + end = end > pluginJSONObjects.size() ? pluginJSONObjects.size() : end; + ret.put(Plugin.PLUGINS, pluginJSONObjects.subList(start, end)); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets plugins failed", e); + + throw new ServiceException(e); + } + } + + /** + * get the setting(json formatter) of the plugin by the specified pluginoId. + * + * @param pluginId the specified pluginId + * @return the {@link AbstractPlugin} + * @throws ServiceException service exception + * @throws JSONException json exception + */ + public String getPluginSetting(final String pluginId) throws ServiceException, JSONException { + + JSONObject ret = null; + + try { + ret = pluginRepository.get(pluginId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "get plugin[" + pluginId + "] fail"); + throw new ServiceException("get plugin[" + pluginId + "] fail"); + + } + + if (ret == null) { + LOGGER.log(Level.ERROR, "can not find plugin[" + pluginId + "]"); + throw new ServiceException("can not find plugin[" + pluginId + "]"); + } + + return ret.optString(Plugin.PLUGIN_SETTING); + } +} diff --git a/src/main/java/org/b3log/solo/service/PreferenceMgmtService.java b/src/main/java/org/b3log/solo/service/PreferenceMgmtService.java new file mode 100644 index 00000000..787e79d0 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/PreferenceMgmtService.java @@ -0,0 +1,237 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Locales; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.Locale; + +/** + * Preference management service. + * + * @author Liang Ding + * @version 1.4.0.2, Aug 18, 2019 + * @since 0.4.0 + */ +@Service +public class PreferenceMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(PreferenceMgmtService.class); + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Updates the preference with the specified preference. + * + * @param preference the specified preference + * @throws ServiceException service exception + */ + public void updatePreference(final JSONObject preference) throws ServiceException { + final Iterator keys = preference.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + if (preference.isNull(key)) { + throw new ServiceException("A value is null of preference [key=" + key + "]"); + } + } + + final Transaction transaction = optionRepository.beginTransaction(); + + try { + preference.put(Option.ID_C_SIGNS, preference.get(Option.ID_C_SIGNS).toString()); + + final JSONObject oldPreference = optionQueryService.getPreference(); + + final String version = oldPreference.optString(Option.ID_C_VERSION); + preference.put(Option.ID_C_VERSION, version); + + final String localeString = preference.getString(Option.ID_C_LOCALE_STRING); + Latkes.setLocale(new Locale(Locales.getLanguage(localeString), Locales.getCountry(localeString))); + + final JSONObject allowVisitDraftViaPermalinkOpt = optionRepository.get(Option.ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK); + allowVisitDraftViaPermalinkOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK)); + optionRepository.update(Option.ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK, allowVisitDraftViaPermalinkOpt); + + final JSONObject articleListDisplayCountOpt = optionRepository.get(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT); + articleListDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT)); + optionRepository.update(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT, articleListDisplayCountOpt); + + final JSONObject articleListPaginationWindowSizeOpt = optionRepository.get(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE); + articleListPaginationWindowSizeOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE)); + optionRepository.update(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE, articleListPaginationWindowSizeOpt); + + final JSONObject articleListStyleOpt = optionRepository.get(Option.ID_C_ARTICLE_LIST_STYLE); + articleListStyleOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_ARTICLE_LIST_STYLE)); + optionRepository.update(Option.ID_C_ARTICLE_LIST_STYLE, articleListStyleOpt); + + final JSONObject blogSubtitleOpt = optionRepository.get(Option.ID_C_BLOG_SUBTITLE); + blogSubtitleOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_BLOG_SUBTITLE)); + optionRepository.update(Option.ID_C_BLOG_SUBTITLE, blogSubtitleOpt); + + final JSONObject blogTitleOpt = optionRepository.get(Option.ID_C_BLOG_TITLE); + blogTitleOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_BLOG_TITLE)); + optionRepository.update(Option.ID_C_BLOG_TITLE, blogTitleOpt); + + final JSONObject commentableOpt = optionRepository.get(Option.ID_C_COMMENTABLE); + commentableOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_COMMENTABLE)); + optionRepository.update(Option.ID_C_COMMENTABLE, commentableOpt); + + final JSONObject enableArticleUpdateHintOpt = optionRepository.get(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT); + enableArticleUpdateHintOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)); + optionRepository.update(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT, enableArticleUpdateHintOpt); + + final JSONObject externalRelevantArticlesDisplayCountOpt = optionRepository.get(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT); + externalRelevantArticlesDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT, externalRelevantArticlesDisplayCountOpt); + + final JSONObject feedOutputCntOpt = optionRepository.get(Option.ID_C_FEED_OUTPUT_CNT); + feedOutputCntOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_FEED_OUTPUT_CNT)); + optionRepository.update(Option.ID_C_FEED_OUTPUT_CNT, feedOutputCntOpt); + + final JSONObject feedOutputModeOpt = optionRepository.get(Option.ID_C_FEED_OUTPUT_MODE); + feedOutputModeOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_FEED_OUTPUT_MODE)); + optionRepository.update(Option.ID_C_FEED_OUTPUT_MODE, feedOutputModeOpt); + + final JSONObject footerContentOpt = optionRepository.get(Option.ID_C_FOOTER_CONTENT); + footerContentOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_FOOTER_CONTENT)); + optionRepository.update(Option.ID_C_FOOTER_CONTENT, footerContentOpt); + + final JSONObject htmlHeadOpt = optionRepository.get(Option.ID_C_HTML_HEAD); + htmlHeadOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_HTML_HEAD)); + optionRepository.update(Option.ID_C_HTML_HEAD, htmlHeadOpt); + + final JSONObject localeStringOpt = optionRepository.get(Option.ID_C_LOCALE_STRING); + localeStringOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_LOCALE_STRING)); + optionRepository.update(Option.ID_C_LOCALE_STRING, localeStringOpt); + + final JSONObject metaDescriptionOpt = optionRepository.get(Option.ID_C_META_DESCRIPTION); + metaDescriptionOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_META_DESCRIPTION)); + optionRepository.update(Option.ID_C_META_DESCRIPTION, metaDescriptionOpt); + + final JSONObject metaKeywordsOpt = optionRepository.get(Option.ID_C_META_KEYWORDS); + metaKeywordsOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_META_KEYWORDS)); + optionRepository.update(Option.ID_C_META_KEYWORDS, metaKeywordsOpt); + + final JSONObject mostCommentArticleDisplayCountOpt = optionRepository.get(Option.ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT); + mostCommentArticleDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_MOST_COMMENT_ARTICLE_DISPLAY_CNT, mostCommentArticleDisplayCountOpt); + + final JSONObject mostUsedTagDisplayCountOpt = optionRepository.get(Option.ID_C_MOST_USED_TAG_DISPLAY_CNT); + mostUsedTagDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_MOST_USED_TAG_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_MOST_USED_TAG_DISPLAY_CNT, mostUsedTagDisplayCountOpt); + + final JSONObject mostViewArticleDisplayCountOpt = optionRepository.get(Option.ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT); + mostViewArticleDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_MOST_VIEW_ARTICLE_DISPLAY_CNT, mostViewArticleDisplayCountOpt); + + final JSONObject noticeBoardOpt = optionRepository.get(Option.ID_C_NOTICE_BOARD); + noticeBoardOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_NOTICE_BOARD)); + optionRepository.update(Option.ID_C_NOTICE_BOARD, noticeBoardOpt); + + final JSONObject randomArticlesDisplayCountOpt = optionRepository.get(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT); + randomArticlesDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT, randomArticlesDisplayCountOpt); + + final JSONObject recentArticleDisplayCountOpt = optionRepository.get(Option.ID_C_RECENT_ARTICLE_DISPLAY_CNT); + recentArticleDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_RECENT_ARTICLE_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_RECENT_ARTICLE_DISPLAY_CNT, recentArticleDisplayCountOpt); + + final JSONObject recentCommentDisplayCountOpt = optionRepository.get(Option.ID_C_RECENT_COMMENT_DISPLAY_CNT); + recentCommentDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_RECENT_COMMENT_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_RECENT_COMMENT_DISPLAY_CNT, recentCommentDisplayCountOpt); + + final JSONObject relevantArticlesDisplayCountOpt = optionRepository.get(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT); + relevantArticlesDisplayCountOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT)); + optionRepository.update(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT, relevantArticlesDisplayCountOpt); + + final JSONObject signsOpt = optionRepository.get(Option.ID_C_SIGNS); + signsOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_SIGNS)); + optionRepository.update(Option.ID_C_SIGNS, signsOpt); + + final JSONObject timeZoneIdOpt = optionRepository.get(Option.ID_C_TIME_ZONE_ID); + timeZoneIdOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_TIME_ZONE_ID)); + optionRepository.update(Option.ID_C_TIME_ZONE_ID, timeZoneIdOpt); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_VERSION)); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + final JSONObject faviconURLOpt = optionRepository.get(Option.ID_C_FAVICON_URL); + faviconURLOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_FAVICON_URL)); + optionRepository.update(Option.ID_C_FAVICON_URL, faviconURLOpt); + + final JSONObject syncGitHubOpt = optionRepository.get(Option.ID_C_SYNC_GITHUB); + syncGitHubOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_SYNC_GITHUB)); + optionRepository.update(Option.ID_C_SYNC_GITHUB, syncGitHubOpt); + + final JSONObject pullGitHubOpt = optionRepository.get(Option.ID_C_PULL_GITHUB); + pullGitHubOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_PULL_GITHUB)); + optionRepository.update(Option.ID_C_PULL_GITHUB, pullGitHubOpt); + + final JSONObject hljsThemeOpt = optionRepository.get(Option.ID_C_HLJS_THEME); + hljsThemeOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_HLJS_THEME)); + optionRepository.update(Option.ID_C_HLJS_THEME, hljsThemeOpt); + + final JSONObject customVarsOpt = optionRepository.get(Option.ID_C_CUSTOM_VARS); + customVarsOpt.put(Option.OPTION_VALUE, preference.optString(Option.ID_C_CUSTOM_VARS)); + optionRepository.update(Option.ID_C_CUSTOM_VARS, customVarsOpt); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates preference failed", e); + throw new ServiceException(langPropsService.get("updateFailLabel")); + } + + LOGGER.log(Level.DEBUG, "Updates preference successfully"); + } +} diff --git a/src/main/java/org/b3log/solo/service/SkinMgmtService.java b/src/main/java/org/b3log/solo/service/SkinMgmtService.java new file mode 100644 index 00000000..d5778715 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/SkinMgmtService.java @@ -0,0 +1,117 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.util.Skins; +import org.json.JSONObject; + +import java.util.Set; + +/** + * Skin management service. + * + * @author Liang Ding + * @version 1.0.0.1, Jun 13, 2019 + * @since 3.5.0 + */ +@Service +public class SkinMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SkinMgmtService.class); + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Loads skins. + * + * @param skin the specified skin + * @throws Exception exception + */ + public void loadSkins(final JSONObject skin) throws Exception { + final Set skinDirNames = Skins.getSkinDirNames(); + final String currentSkinDirName = skin.optString(Option.ID_C_SKIN_DIR_NAME); + if (!skinDirNames.contains(currentSkinDirName)) { + LOGGER.log(Level.WARN, "Not found skin [dirName={0}] configured, try to use default skin [dirName=" + + Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME + "] instead", currentSkinDirName); + if (!skinDirNames.contains(Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME)) { + LOGGER.log(Level.ERROR, "Not found default skin [dirName=" + Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME + + "], please redeploy your Solo and make sure contains the default skin. If you are using git, try to pull with 'git pull --recurse-submodules'"); + System.exit(-1); + } + + skin.put(Option.ID_C_SKIN_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + updateSkin(skin); + } + } + + /** + * Updates the skin with the specified skin. + * + * @param skin the specified skin + * @throws ServiceException service exception + */ + public void updateSkin(final JSONObject skin) throws ServiceException { + final Transaction transaction = optionRepository.beginTransaction(); + try { + final JSONObject skinDirNameOpt = optionRepository.get(Option.ID_C_SKIN_DIR_NAME); + skinDirNameOpt.put(Option.OPTION_VALUE, skin.optString(Option.ID_C_SKIN_DIR_NAME)); + optionRepository.update(Option.ID_C_SKIN_DIR_NAME, skinDirNameOpt); + + final JSONObject mobileSkinDirNameOpt = optionRepository.get(Option.ID_C_MOBILE_SKIN_DIR_NAME); + mobileSkinDirNameOpt.put(Option.OPTION_VALUE, skin.optString(Option.ID_C_MOBILE_SKIN_DIR_NAME)); + optionRepository.update(Option.ID_C_MOBILE_SKIN_DIR_NAME, mobileSkinDirNameOpt); + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates skin failed", e); + throw new ServiceException(langPropsService.get("updateFailLabel")); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/StatisticMgmtService.java b/src/main/java/org/b3log/solo/service/StatisticMgmtService.java new file mode 100644 index 00000000..60eb8666 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/StatisticMgmtService.java @@ -0,0 +1,282 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.util.Requests; +import org.b3log.latke.util.URLs; +import org.b3log.solo.cache.StatisticCache; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Statistic management service. + * + * @author Liang Ding + * @version 2.0.1.3, Jan 28, 2019 + * @since 0.5.0 + */ +@Service +public class StatisticMgmtService { + + /** + * Online visitor cache. + *

+ * <ip, recentTime> + *

+ */ + public static final Map ONLINE_VISITORS = new ConcurrentHashMap<>(); + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(StatisticMgmtService.class); + + /** + * Online visitor expiration in 5 minutes. + */ + private static final int ONLINE_VISITOR_EXPIRATION = 300000; + + /** + * Cookie expiry of "visited". + */ + private static final int COOKIE_EXPIRY = 60 * 60 * 24; // 24 hours + + /** + * Option repository. + */ + @Inject + private OptionRepository optionRepository; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Statistic cache. + */ + @Inject + private StatisticCache statisticCache; + + /** + * Removes the expired online visitor. + */ + public static void removeExpiredOnlineVisitor() { + final long currentTimeMillis = System.currentTimeMillis(); + + final Iterator> iterator = ONLINE_VISITORS.entrySet().iterator(); + + while (iterator.hasNext()) { + final Map.Entry onlineVisitor = iterator.next(); + + if (currentTimeMillis > (onlineVisitor.getValue() + ONLINE_VISITOR_EXPIRATION)) { + iterator.remove(); + LOGGER.log(Level.TRACE, "Removed online visitor[ip={0}]", onlineVisitor.getKey()); + } + } + + LOGGER.log(Level.DEBUG, "Current online visitor count [{0}]", ONLINE_VISITORS.size()); + } + + /** + * Determines whether the specified request has been served. + *

+ * A "served request" is a request a URI as former one. For example, if a client is request "/test", all requests from the client + * subsequent in 24 hours will be treated as served requests, requested URIs save in client cookie (name: "visited"). + *

+ *

+ * If the specified request has not been served, appends the request URI in client cookie. + *

+ *

+ * Sees this issue (https://github.com/b3log/solo/issues/44) for more details. + *

+ * + * @param context the specified request context + * @param response the specified response + * @return {@code true} if the specified request has been served, returns {@code false} otherwise + */ + public static boolean hasBeenServed(final RequestContext context, final HttpServletResponse response) { + final HttpServletRequest request = context.getRequest(); + final Cookie[] cookies = request.getCookies(); + if (null == cookies || 0 == cookies.length) { + return false; + } + + Cookie cookie; + boolean needToCreate = true; + boolean needToAppend = true; + JSONArray cookieJSONArray = null; + + try { + for (int i = 0; i < cookies.length; i++) { + cookie = cookies[i]; + + if (!"visited".equals(cookie.getName())) { + continue; + } + + final String value = URLs.decode(cookie.getValue()); + cookieJSONArray = new JSONArray(value); + if (null == cookieJSONArray || 0 == cookieJSONArray.length()) { + return false; + } + + needToCreate = false; + + for (int j = 0; j < cookieJSONArray.length(); j++) { + final String visitedURL = cookieJSONArray.optString(j); + + if (request.getRequestURI().equals(visitedURL)) { + needToAppend = false; + return true; + } + } + } + + if (needToCreate) { + final StringBuilder builder = new StringBuilder("[").append("\"").append(request.getRequestURI()).append("\"]"); + final Cookie c = new Cookie("visited", URLs.encode(builder.toString())); + c.setMaxAge(COOKIE_EXPIRY); + c.setPath("/"); + response.addCookie(c); + } else if (needToAppend) { + cookieJSONArray.put(request.getRequestURI()); + + final Cookie c = new Cookie("visited", URLs.encode(cookieJSONArray.toString())); + c.setMaxAge(COOKIE_EXPIRY); + c.setPath("/"); + response.addCookie(c); + } + } catch (final Exception e) { + LOGGER.log(Level.WARN, "Parses cookie failed, clears the cookie[name=visited]"); + + final Cookie c = new Cookie("visited", null); + c.setMaxAge(0); + c.setPath("/"); + + response.addCookie(c); + } + + return false; + } + + /** + * Blog statistic view count +1. + *

+ * If it is a search engine bot made the specified request, will NOT increment blog statistic view count. + *

+ *

+ * There is a cron job (/console/stat/viewcnt) to flush the blog view count from cache to datastore. + *

+ * + * @param context the specified request context + * @param response the specified response + * @throws ServiceException service exception + */ + public void incBlogViewCount(final RequestContext context, final HttpServletResponse response) throws ServiceException { + if (Solos.isBot(context.getRequest())) { + return; + } + + if (hasBeenServed(context, response)) { + return; + } + + final Transaction transaction = optionRepository.beginTransaction(); + JSONObject statistic; + try { + statistic = optionRepository.get(Option.ID_C_STATISTIC_BLOG_VIEW_COUNT); + if (null == statistic) { + return; + } + + LOGGER.log(Level.TRACE, "Before inc blog view count is [{0}]", statistic); + + statistic.put(Option.OPTION_VALUE, statistic.optInt(Option.OPTION_VALUE) + 1); + + updateStatistic(Option.ID_C_STATISTIC_BLOG_VIEW_COUNT, statistic); + + transaction.commit(); + } catch (final RepositoryException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates blog view count failed", e); + + return; + } + + LOGGER.log(Level.TRACE, "Inced blog view count[statistic={0}]", statistic); + } + + /** + * Refreshes online visitor count for the specified request. + * + * @param request the specified request + */ + public void onlineVisitorCount(final HttpServletRequest request) { + if (Solos.isBot(request)) { + return; + } + + final String remoteAddr = Requests.getRemoteAddr(request); + LOGGER.log(Level.DEBUG, "Current request [IP={0}]", remoteAddr); + ONLINE_VISITORS.put(remoteAddr, System.currentTimeMillis()); + LOGGER.log(Level.DEBUG, "Current online visitor count [{0}]", ONLINE_VISITORS.size()); + } + + /** + * Updates the statistic with the specified statistic. + * + * @param id the specified statistic id + * @param statistic the specified statistic + * @throws RepositoryException repository exception + */ + private void updateStatistic(final String id, final JSONObject statistic) throws RepositoryException { + optionRepository.update(id, statistic); + statisticCache.clear(); + } +} diff --git a/src/main/java/org/b3log/solo/service/StatisticQueryService.java b/src/main/java/org/b3log/solo/service/StatisticQueryService.java new file mode 100644 index 00000000..22b69425 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/StatisticQueryService.java @@ -0,0 +1,95 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.FilterOperator; +import org.b3log.latke.repository.PropertyFilter; +import org.b3log.latke.repository.Query; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.CommentRepository; +import org.json.JSONObject; + +/** + * Statistic query service. + * + * @author Liang Ding + * @version 2.0.0.2, Jan 28, 2019 + * @since 0.5.0 + */ +@Service +public class StatisticQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(StatisticQueryService.class); + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Article repository. + */ + @Inject + private ArticleRepository articleRepository; + + /** + * Comment repository. + */ + @Inject + private CommentRepository commentRepository; + + /** + * Gets the online visitor count. + * + * @return online visitor count + */ + public static int getOnlineVisitorCount() { + return StatisticMgmtService.ONLINE_VISITORS.size(); + } + + /** + * Gets the statistic. + * + * @return statistic, returns {@code null} if not found + */ + public JSONObject getStatistic() { + try { + final JSONObject ret = optionQueryService.getOptions(Option.CATEGORY_C_STATISTIC); + final long publishedArticleCount = articleRepository.count(new Query().setFilter(new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.EQUAL, Article.ARTICLE_STATUS_C_PUBLISHED))); + ret.put(Option.ID_T_STATISTIC_PUBLISHED_ARTICLE_COUNT, publishedArticleCount); + final long commentCount = commentRepository.count(new Query()); + ret.put(Option.ID_T_STATISTIC_PUBLISHED_BLOG_COMMENT_COUNT, commentCount); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets statistic failed", e); + + return null; + } + } +} diff --git a/src/main/java/org/b3log/solo/service/TagMgmtService.java b/src/main/java/org/b3log/solo/service/TagMgmtService.java new file mode 100644 index 00000000..8e1198d9 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/TagMgmtService.java @@ -0,0 +1,104 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.repository.CategoryTagRepository; +import org.b3log.solo.repository.TagArticleRepository; +import org.b3log.solo.repository.TagRepository; +import org.json.JSONObject; + +import java.util.List; + +/** + * Tag management service. + * + * @author Liang Ding + * @version 1.0.0.3, Jan 28, 2019 + * @since 0.4.0 + */ +@Service +public class TagMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(TagMgmtService.class); + + /** + * Tag query service. + */ + @Inject + private TagQueryService tagQueryService; + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Category-tag repository. + */ + @Inject + private CategoryTagRepository categoryTagRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Removes all unused tags. + * + * @throws ServiceException if get tags failed, or remove failed + */ + public void removeUnusedTags() throws ServiceException { + final Transaction transaction = tagRepository.beginTransaction(); + + try { + final List tags = tagQueryService.getTags(); + for (int i = 0; i < tags.size(); i++) { + final JSONObject tag = tags.get(i); + final String tagId = tag.optString(Keys.OBJECT_ID); + final int articleCount = tagArticleRepository.getArticleCount(tagId); + if (1 > articleCount) { + categoryTagRepository.removeByTagId(tagId); + tagRepository.remove(tagId); + } + } + + transaction.commit(); + } catch (final Exception e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Removes unused tags failed", e); + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/TagQueryService.java b/src/main/java/org/b3log/solo/service/TagQueryService.java new file mode 100644 index 00000000..c8ca227b --- /dev/null +++ b/src/main/java/org/b3log/solo/service/TagQueryService.java @@ -0,0 +1,148 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.model.Tag; +import org.b3log.solo.repository.TagArticleRepository; +import org.b3log.solo.repository.TagRepository; +import org.json.JSONObject; + +import java.util.List; + +/** + * Tag query service. + * + * @author Liang Ding + * @version 1.1.0.7, Jun 20, 2019 + * @since 0.4.0 + */ +@Service +public class TagQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(TagQueryService.class); + + /** + * Tag repository. + */ + @Inject + private TagRepository tagRepository; + + /** + * Tag-Article repository. + */ + @Inject + private TagArticleRepository tagArticleRepository; + + /** + * Gets article count of a tag specified by the given tag id. + * + * @param tagId the given tag id + * @return article count, returns {@code -1} if occurred an exception + */ + public int getArticleCount(final String tagId) { + return tagArticleRepository.getArticleCount(tagId); + } + + /** + * Gets a tag by the specified tag title. + * + * @param tagTitle the specified tag title + * @return for example,
+     * {
+     *     "tag": {
+     *         "oId": "",
+     *         "tagTitle": "",
+     *         "tagPublishedRefCount": int
+     *     }
+     * }
+     * 
, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getTagByTitle(final String tagTitle) throws ServiceException { + try { + final JSONObject tag = tagRepository.getByTitle(tagTitle); + if (null == tag) { + return null; + } + + final JSONObject ret = new JSONObject(); + ret.put(Tag.TAG, tag); + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets an article failed", e); + throw new ServiceException(e); + } + } + + /** + * Gets the count of tags. + * + * @return count of tags + */ + public long getTagCount() { + try { + return tagRepository.count(); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets tags failed", e); + + return 0; + } + } + + /** + * Gets all tags. + * + * @return for example,
+     * [
+     *     {"tagTitle": "", ....},
+     *     ....
+     * ]
+     * 
, returns an empty list if not found + * @throws ServiceException service exception + */ + public List getTags() throws ServiceException { + try { + final Query query = new Query().setPageCount(1); + + final List ret = tagRepository.getList(query); + for (final JSONObject tag : ret) { + final String tagId = tag.optString(Keys.OBJECT_ID); + final int articleCount = tagArticleRepository.getPublishedArticleCount(tagId); + tag.put(Tag.TAG_T_PUBLISHED_REFERENCE_COUNT, articleCount); + } + + return ret; + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets tags failed", e); + + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/UpgradeService.java b/src/main/java/org/b3log/solo/service/UpgradeService.java new file mode 100644 index 00000000..bb48cc7f --- /dev/null +++ b/src/main/java/org/b3log/solo/service/UpgradeService.java @@ -0,0 +1,104 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.annotation.Service; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Option; +import org.b3log.solo.upgrade.*; +import org.json.JSONObject; + +/** + * Upgrade service. + * + * @author Liang Ding + * @version 1.2.1.11, Sep 18, 2019 + * @since 1.2.0 + */ +@Service +public class UpgradeService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(UpgradeService.class); + + /** + * Option Query Service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Upgrades if need. + */ + public void upgrade() { + try { + final JSONObject preference = optionQueryService.getPreference(); + if (null == preference) { + return; + } + + final String currentVer = preference.getString(Option.ID_C_VERSION); // 数据库中的版本 + if (SoloServletListener.VERSION.equals(currentVer)) { + // 如果数据库中的版本和运行时版本一致则说明已经是最新版 + return; + } + + // 如果版本较老,则调用对应的升级程序进行升级,并贯穿升级下去直到最新版 + switch (currentVer) { + case "2.9.9": + V299_300.perform(); + case "3.0.0": + V300_310.perform(); + case "3.1.0": + V310_320.perform(); + case "3.2.0": + V320_330.perform(); + case "3.3.0": + V330_340.perform(); + case "3.4.0": + V340_350.perform(); + case "3.5.0": + V350_360.perform(); + case "3.6.0": + V360_361.perform(); + case "3.6.1": + V361_362.perform(); + case "3.6.2": + V362_363.perform(); + case "3.6.3": + V363_364.perform(); + case "3.6.4": + V364_365.perform(); + + break; + default: + LOGGER.log(Level.ERROR, "Please upgrade to v3.0.0 first"); + System.exit(-1); + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed, please contact the Solo developers or reports this " + + "issue: https://github.com/b3log/solo/issues/new", e); + System.exit(-1); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/UserMgmtService.java b/src/main/java/org/b3log/solo/service/UserMgmtService.java new file mode 100644 index 00000000..34343d42 --- /dev/null +++ b/src/main/java/org/b3log/solo/service/UserMgmtService.java @@ -0,0 +1,340 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Strings; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.repository.UserRepository; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; + +/** + * User management service. + * + * @author Liang Ding + * @author DASHU + * @author nanolikeyou + * @version 1.1.0.19, Jun 6, 2019 + * @since 0.4.0 + */ +@Service +public class UserMgmtService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(UserMgmtService.class); + + /** + * Length of hashed password. + */ + private static final int HASHED_PASSWORD_LENGTH = 32; + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * Language service. + */ + @Inject + private LangPropsService langPropsService; + + /** + * Option query service. + */ + @Inject + private OptionQueryService optionQueryService; + + /** + * Option management service. + */ + @Inject + private OptionMgmtService optionMgmtService; + + /** + * Init service. + */ + @Inject + private InitService initService; + + /** + * Refresh usite. 展示站点连接 https://github.com/b3log/solo/issues/12719 + */ + public void refreshUSite() { + if (!initService.isInited()) { + return; + } + + JSONObject admin; + try { + admin = userRepository.getAdmin(); + } catch (final Exception e) { + return; + } + + JSONObject usite; + try { + final JSONObject requestJSON = new JSONObject(). + put(User.USER_NAME, admin.optString(User.USER_NAME)). + put(UserExt.USER_B3_KEY, admin.optString(UserExt.USER_B3_KEY)); + final HttpResponse res = HttpRequest.post("https://hacpai.com/user/usite").trustAllCerts(true). + connectionTimeout(3000).timeout(7000).header("User-Agent", Solos.USER_AGENT). + body(requestJSON.toString()).send(); + if (HttpServletResponse.SC_OK != res.statusCode()) { + return; + } + res.charset("UTF-8"); + final JSONObject result = new JSONObject(res.bodyText()); + if (0 != result.optInt(Keys.STATUS_CODE)) { + return; + } + usite = result.optJSONObject(Common.DATA); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets usite failed", e); + + return; + } + + JSONObject usiteOpt = optionQueryService.getOptionById(Option.ID_C_USITE); + if (null == usiteOpt) { + usiteOpt = new JSONObject(); + usiteOpt.put(Keys.OBJECT_ID, Option.ID_C_USITE); + usiteOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_HACPAI); + } + usiteOpt.put(Option.OPTION_VALUE, usite.toString()); + try { + optionMgmtService.addOrUpdateOption(usiteOpt); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Updates usite option failed", e); + + return; + } + } + + /** + * Updates a user by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "oId": "", + * "userName": "", + * "userRole": "", + * "userURL": "", + * "userB3Key": "", + * "userGitHubId": "" // optional + * @throws ServiceException service exception + */ + public void updateUser(final JSONObject requestJSONObject) throws ServiceException { + final Transaction transaction = userRepository.beginTransaction(); + + try { + final String oldUserId = requestJSONObject.optString(Keys.OBJECT_ID); + final JSONObject oldUser = userRepository.get(oldUserId); + if (null == oldUser) { + throw new ServiceException(langPropsService.get("updateFailLabel")); + } + + final String userName = requestJSONObject.optString(User.USER_NAME); + if (UserExt.invalidUserName(userName)) { + throw new ServiceException(langPropsService.get("userNameInvalidLabel")); + } + + JSONObject mayBeAnother = userRepository.getByUserName(userName); + if (null != mayBeAnother && !mayBeAnother.optString(Keys.OBJECT_ID).equals(oldUserId)) { + throw new ServiceException(langPropsService.get("duplicatedUserNameLabel")); + } + oldUser.put(User.USER_NAME, userName); + + final String userRole = requestJSONObject.optString(User.USER_ROLE); + oldUser.put(User.USER_ROLE, userRole); + + final String userURL = requestJSONObject.optString(User.USER_URL); + oldUser.put(User.USER_URL, userURL); + + final String userAvatar = requestJSONObject.optString(UserExt.USER_AVATAR); + oldUser.put(UserExt.USER_AVATAR, userAvatar); + + final String userB3Key = requestJSONObject.optString(UserExt.USER_B3_KEY); + oldUser.put(UserExt.USER_B3_KEY, userB3Key); + + final String userGitHubId = requestJSONObject.optString(UserExt.USER_GITHUB_ID); + if (StringUtils.isNotBlank(userGitHubId)) { + oldUser.put(UserExt.USER_GITHUB_ID, userGitHubId); + } + + userRepository.update(oldUserId, oldUser); + transaction.commit(); + } catch (final RepositoryException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates a user failed", e); + throw new ServiceException(e); + } + } + + /** + * Switches the user role between "defaultRole" and "visitorRole" by the specified user id. + * + * @param userId the specified user id + * @throws ServiceException exception + * @see User + */ + public void changeRole(final String userId) throws ServiceException { + final Transaction transaction = userRepository.beginTransaction(); + + try { + final JSONObject oldUser = userRepository.get(userId); + + if (null == oldUser) { + throw new ServiceException(langPropsService.get("updateFailLabel")); + } + + final String role = oldUser.optString(User.USER_ROLE); + + if (Role.VISITOR_ROLE.equals(role)) { + oldUser.put(User.USER_ROLE, Role.DEFAULT_ROLE); + } else if (Role.DEFAULT_ROLE.equals(role)) { + oldUser.put(User.USER_ROLE, Role.VISITOR_ROLE); + } + + userRepository.update(userId, oldUser, User.USER_ROLE); + + transaction.commit(); + } catch (final RepositoryException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Updates a user failed", e); + throw new ServiceException(e); + } + } + + /** + * Adds a user with the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "userName": "", + * "userURL": "", // optional, uses 'servePath' instead if not specified + * "userRole": "", // optional, uses {@value Role#DEFAULT_ROLE} instead if not specified + * "userAvatar": "", // optional, users generated gravatar url instead if not specified + * "userGitHubId": "", + * "userB3Key": "" + * @return generated user id + * @throws ServiceException service exception + */ + public synchronized String addUser(final JSONObject requestJSONObject) throws ServiceException { + final Transaction transaction = userRepository.beginTransaction(); + + try { + final String userName = requestJSONObject.optString(User.USER_NAME); + if (UserExt.invalidUserName(userName)) { + throw new ServiceException(langPropsService.get("userNameInvalidLabel")); + } + + JSONObject duplicatedUser = userRepository.getByUserName(userName); + if (null != duplicatedUser) { + if (transaction.isActive()) { + transaction.rollback(); + } + + throw new ServiceException(langPropsService.get("duplicatedUserNameLabel")); + } + final JSONObject user = new JSONObject(); + user.put(User.USER_NAME, userName); + + String userURL = requestJSONObject.optString(User.USER_URL); + if (StringUtils.isBlank(userURL)) { + userURL = Latkes.getServePath(); + } + if (!Strings.isURL(userURL)) { + throw new ServiceException(langPropsService.get("urlInvalidLabel")); + } + user.put(User.USER_URL, userURL); + + final String roleName = requestJSONObject.optString(User.USER_ROLE, Role.DEFAULT_ROLE); + user.put(User.USER_ROLE, roleName); + + final String userAvatar = requestJSONObject.optString(UserExt.USER_AVATAR); + user.put(UserExt.USER_AVATAR, userAvatar); + + final String userGitHubId = requestJSONObject.optString(UserExt.USER_GITHUB_ID); + user.put(UserExt.USER_GITHUB_ID, userGitHubId); + + final String userB3Key = requestJSONObject.optString(UserExt.USER_B3_KEY); + user.put(UserExt.USER_B3_KEY, userB3Key); + + userRepository.add(user); + transaction.commit(); + + return user.optString(Keys.OBJECT_ID); + } catch (final RepositoryException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Adds a user failed", e); + throw new ServiceException(e); + } + } + + /** + * Removes a user specified by the given user id. + * + * @param userId the given user id + * @throws ServiceException service exception + */ + public void removeUser(final String userId) throws ServiceException { + final Transaction transaction = userRepository.beginTransaction(); + try { + userRepository.remove(userId); + + transaction.commit(); + } catch (final RepositoryException e) { + if (transaction.isActive()) { + transaction.rollback(); + } + + LOGGER.log(Level.ERROR, "Removes a user [id=" + userId + "] failed", e); + throw new ServiceException(e); + } + } +} diff --git a/src/main/java/org/b3log/solo/service/UserQueryService.java b/src/main/java/org/b3log/solo/service/UserQueryService.java new file mode 100644 index 00000000..45b3db7f --- /dev/null +++ b/src/main/java/org/b3log/solo/service/UserQueryService.java @@ -0,0 +1,228 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.Inject; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.FilterOperator; +import org.b3log.latke.repository.PropertyFilter; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.RepositoryException; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.service.annotation.Service; +import org.b3log.latke.util.Paginator; +import org.b3log.latke.util.URLs; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.repository.UserRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * User query service. + * + * @author Liang Ding + * @version 1.1.0.0, Feb 3, 2019 + * @since 0.4.0 + */ +@Service +public class UserQueryService { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(UserQueryService.class); + + /** + * User repository. + */ + @Inject + private UserRepository userRepository; + + /** + * User management service. + */ + @Inject + private UserMgmtService userMgmtService; + + /** + * Gets a user by the specified GitHub id. + * + * @param githubId the specified GitHub id + * @return user, returns {@code null} if not found + */ + public JSONObject getUserByGitHubId(final String githubId) { + try { + return userRepository.getFirst(new Query().setFilter(new PropertyFilter(UserExt.USER_GITHUB_ID, FilterOperator.EQUAL, githubId))); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets a user by GitHub id [" + githubId + "] failed", e); + + return null; + } + } + + /** + * Gets the administrator. + * + * @return administrator, returns {@code null} if not found + * @throws ServiceException service exception + */ + public JSONObject getAdmin() throws ServiceException { + try { + return userRepository.getAdmin(); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets admin failed", e); + throw new ServiceException(e); + } + } + + /** + * Gets a user by the specified user name. + * + * @param userName the specified user name + * @return user, returns {@code null} if not found + */ + public JSONObject getUserByName(final String userName) { + try { + return userRepository.getByUserName(userName); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets a user by username [" + userName + "] failed", e); + + return null; + } + } + + /** + * Gets users by the specified request json object. + * + * @param requestJSONObject the specified request json object, for example, + * "paginationCurrentPageNum": 1, + * "paginationPageSize": 20, + * "paginationWindowSize": 10 + * @return for example, + *
+     * {
+     *     "pagination": {
+     *         "paginationPageCount": 100,
+     *         "paginationPageNums": [1, 2, 3, 4, 5]
+     *     },
+     *     "users": [{
+     *         "oId": "",
+     *         "userName": "",
+     *         "roleName": ""
+     *      }, ....]
+     * }
+     * 
+ * @throws ServiceException service exception + * @see Pagination + */ + public JSONObject getUsers(final JSONObject requestJSONObject) throws ServiceException { + final JSONObject ret = new JSONObject(); + + final int currentPageNum = requestJSONObject.optInt(Pagination.PAGINATION_CURRENT_PAGE_NUM); + final int pageSize = requestJSONObject.optInt(Pagination.PAGINATION_PAGE_SIZE); + final int windowSize = requestJSONObject.optInt(Pagination.PAGINATION_WINDOW_SIZE); + final Query query = new Query().setPage(currentPageNum, pageSize); + + JSONObject result; + try { + result = userRepository.get(query); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets users failed", e); + + throw new ServiceException(e); + } + + final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT); + final JSONObject pagination = new JSONObject(); + ret.put(Pagination.PAGINATION, pagination); + final List pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize); + pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount); + pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums); + final JSONArray users = result.optJSONArray(Keys.RESULTS); + ret.put(User.USERS, users); + + return ret; + } + + /** + * Gets a user by the specified user id. + * + * @param userId the specified user id + * @return for example, + *
+     * {
+     *     "user": {
+     *         "oId": "",
+     *         "userName": ""
+     *     }
+     * }
+     * 
, returns {@code null} if not found + */ + public JSONObject getUser(final String userId) { + final JSONObject ret = new JSONObject(); + + JSONObject user; + try { + user = userRepository.get(userId); + } catch (final RepositoryException e) { + LOGGER.log(Level.ERROR, "Gets a user failed", e); + + return null; + } + + if (null == user) { + return null; + } + + ret.put(User.USER, user); + + return ret; + } + + /** + * Gets the URL of user logout. + * + * @return logout URL, returns {@code null} if the user is not logged in + */ + public String getLogoutURL() { + String to = Latkes.getServePath(); + to = URLs.encode(to); + + return Latkes.getContextPath() + "/logout?referer=" + to; + } + + /** + * Gets the URL of user login. + * + * @param redirectURL redirect URL after logged in + * @return login URL + */ + public String getLoginURL(final String redirectURL) { + String to = Latkes.getServePath(); + to = URLs.encode(to + redirectURL); + + return Latkes.getContextPath() + "/start?referer=" + to; + } +} diff --git a/src/main/java/org/b3log/solo/service/package-info.java b/src/main/java/org/b3log/solo/service/package-info.java new file mode 100644 index 00000000..935b424b --- /dev/null +++ b/src/main/java/org/b3log/solo/service/package-info.java @@ -0,0 +1,4 @@ +/** + * Services. + */ +package org.b3log.solo.service; diff --git a/src/main/java/org/b3log/solo/upgrade/V299_300.java b/src/main/java/org/b3log/solo/upgrade/V299_300.java new file mode 100644 index 00000000..4dd285da --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V299_300.java @@ -0,0 +1,131 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.latke.util.CollectionUtils; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.repository.UserRepository; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.Set; + +/** + * Upgrade script from v2.9.9 to v3.0.0. + * + * @author Liang Ding + * @version 1.0.0.0, Feb 28, 2019 + * @since 3.0.0 + */ +public final class V299_300 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V299_300.class); + + /** + * Performs upgrade from v2.9.9 to v3.0.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + LOGGER.log(Level.INFO, "Upgrading from version [2.9.9] to version [3.0.0]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + final UserRepository userRepository = beanManager.getReference(UserRepository.class); + + try { + Connection connection = Connections.getConnection(); + Statement statement = connection.createStatement(); + + final String tablePrefix = Latkes.getLocalProperty("jdbc.tablePrefix") + "_"; + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "user` ADD COLUMN `userB3Key` VARCHAR(64) DEFAULT '' NOT NULL"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "user` ADD COLUMN `userGitHubId` VARCHAR(32) DEFAULT '' NOT NULL"); + statement.close(); + connection.commit(); + connection.close(); + + final Transaction transaction = optionRepository.beginTransaction(); + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, "3.0.0"); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + final JSONObject oauthGitHubOpt = optionRepository.get("oauthGitHub"); + if (null != oauthGitHubOpt) { + String value = oauthGitHubOpt.optString(Option.OPTION_VALUE); + final Set githubs = CollectionUtils.jsonArrayToSet(new JSONArray(value)); + for (final String pair : githubs) { + final String githubId = pair.split(":@:")[0]; + final String userId = pair.split(":@:")[1]; + final JSONObject user = userRepository.get(userId); + user.put(UserExt.USER_GITHUB_ID, githubId); + user.put(UserExt.USER_B3_KEY, githubId); + userRepository.update(userId, user); + } + } + optionRepository.remove("oauthGitHub"); + + final String b3Key = optionRepository.get("keyOfSolo").optString(Option.OPTION_VALUE); + final JSONObject admin = userRepository.getAdmin(); + admin.put(UserExt.USER_B3_KEY, b3Key); + userRepository.update(admin.optString(Keys.OBJECT_ID), admin); + optionRepository.remove("keyOfSolo"); + + optionRepository.remove("qiniuAccessKey"); + optionRepository.remove("qiniuBucket"); + optionRepository.remove("qiniuDomain"); + optionRepository.remove("qiniuSecretKey"); + optionRepository.remove("ossServer"); + optionRepository.remove("aliyunAccessKey"); + optionRepository.remove("aliyunSecretKey"); + optionRepository.remove("aliyunDomain"); + optionRepository.remove("aliyunBucket"); + optionRepository.remove("editorType"); + + transaction.commit(); + + connection = Connections.getConnection(); + statement = connection.createStatement(); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "article` DROP COLUMN `articleEditorType`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "page` DROP COLUMN `pageEditorType`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "user` DROP COLUMN `userPassword`"); + statement.close(); + connection.commit(); + connection.close(); + + LOGGER.log(Level.INFO, "Upgraded from version [2.9.9] to version [3.0.0] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [2.9.9] to version [3.0.0]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V300_310.java b/src/main/java/org/b3log/solo/upgrade/V300_310.java new file mode 100644 index 00000000..2a037cb4 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V300_310.java @@ -0,0 +1,110 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.util.Images; +import org.json.JSONObject; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.List; + +/** + * Upgrade script from v3.0.0 to v3.1.0. + * + * @author Liang Ding + * @version 1.0.1.1, Mar 20, 2019 + * @since 3.1.0 + */ +public final class V300_310 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V300_310.class); + + /** + * Performs upgrade from v3.0.0 to v3.1.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + LOGGER.log(Level.INFO, "Upgrading from version [3.0.0] to version [3.1.0]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class); + + try { + Connection connection = Connections.getConnection(); + Statement statement = connection.createStatement(); + + // 文章表新增首图字段 + final String tablePrefix = Latkes.getLocalProperty("jdbc.tablePrefix") + "_"; + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "article` ADD COLUMN `articleAbstractText` TEXT"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "article` ADD COLUMN `articleImg1URL` VARCHAR(255) DEFAULT '' NOT NULL"); + statement.close(); + connection.commit(); + connection.close(); + + final Transaction transaction = optionRepository.beginTransaction(); + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, "3.1.0"); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + // 历史文章使用随机图片填充首图字段 + final List articles = articleRepository.getList(new Query()); + for (final JSONObject article : articles) { + final String imgURL = Images.imageSize(Images.randImage(), Article.ARTICLE_THUMB_IMG_WIDTH, Article.ARTICLE_THUMB_IMG_HEIGHT); + article.put(Article.ARTICLE_IMG1_URL, imgURL); + + final String summary = article.optString(Article.ARTICLE_ABSTRACT); + String summaryText; + if (StringUtils.isBlank(summary)) { + final String content = article.optString(Article.ARTICLE_CONTENT); + summaryText = Article.getAbstractText(content); + article.put(Article.ARTICLE_ABSTRACT, summaryText); + } else { + summaryText = Article.getAbstractText(summary); + } + article.put(Article.ARTICLE_ABSTRACT_TEXT, summaryText); + + articleRepository.update(article.optString(Keys.OBJECT_ID), article); + } + + transaction.commit(); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [3.0.0] to version [3.1.0]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V310_320.java b/src/main/java/org/b3log/solo/upgrade/V310_320.java new file mode 100644 index 00000000..9094ec02 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V310_320.java @@ -0,0 +1,117 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.FilterOperator; +import org.b3log.latke.repository.PropertyFilter; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.List; + +/** + * Upgrade script from v3.1.0 to v3.2.0. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 3, 2019 + * @since 3.2.0 + */ +public final class V310_320 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V310_320.class); + + /** + * Performs upgrade from v3.1.0 to v3.2.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.1.0"; + final String toVer = "3.2.0"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class); + + try { + final String tablePrefix = Latkes.getLocalProperty("jdbc.tablePrefix") + "_"; + + Connection connection = Connections.getConnection(); + Statement statement = connection.createStatement(); + // 重构文章草稿、发布状态 https://github.com/b3log/solo/issues/12669 + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "article` ADD COLUMN `articleStatus` INT DEFAULT 0 NOT NULL"); + statement.close(); + connection.commit(); + connection.close(); + + final Transaction transaction = optionRepository.beginTransaction(); + + optionRepository.remove("adminEmail"); + optionRepository.remove("replyNotiTplBody"); + optionRepository.remove("replyNotiTplSubject"); + + final List drafts = articleRepository.getList(new Query().setFilter(new PropertyFilter("articleIsPublished", FilterOperator.EQUAL, false))); + for (final JSONObject draft : drafts) { + draft.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_DRAFT); + articleRepository.update(draft.optString(Keys.OBJECT_ID), draft); + } + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + transaction.commit(); + + connection = Connections.getConnection(); + statement = connection.createStatement(); + // 移除邮件相关功能 https://github.com/b3log/solo/issues/12690 + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "user` DROP COLUMN `userEmail`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "comment` DROP COLUMN `commentEmail`"); + // 重构文章草稿、发布状态 https://github.com/b3log/solo/issues/12669 + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "article` DROP COLUMN `articleIsPublished`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "article` DROP COLUMN `articleHadBeenPublished`"); + statement.close(); + connection.commit(); + connection.close(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V320_330.java b/src/main/java/org/b3log/solo/upgrade/V320_330.java new file mode 100644 index 00000000..fe60102d --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V320_330.java @@ -0,0 +1,84 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +/** + * Upgrade script from v3.2.0 to v3.3.0. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 6, 2019 + * @since 3.3.0 + */ +public final class V320_330 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V320_330.class); + + /** + * Performs upgrade from v3.2.0 to v3.3.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.2.0"; + final String toVer = "3.3.0"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + optionRepository.remove("allowRegister"); + + JSONObject faviconURLOpt = optionRepository.get(Option.ID_C_FAVICON_URL); + if (null == faviconURLOpt) { + faviconURLOpt = new JSONObject(); + faviconURLOpt.put(Keys.OBJECT_ID, Option.ID_C_FAVICON_URL); + faviconURLOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + faviconURLOpt.put(Option.OPTION_VALUE, Option.DefaultPreference.DEFAULT_FAVICON_URL); + optionRepository.add(faviconURLOpt); + } + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V330_340.java b/src/main/java/org/b3log/solo/upgrade/V330_340.java new file mode 100644 index 00000000..a29e8d75 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V330_340.java @@ -0,0 +1,106 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +/** + * Upgrade script from v3.3.0 to v3.4.0. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 19, 2019 + * @since 3.4.0 + */ +public final class V330_340 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V330_340.class); + + /** + * Performs upgrade from v3.3.0 to v3.4.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.3.0"; + final String toVer = "3.4.0"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + final String tablePrefix = Latkes.getLocalProperty("jdbc.tablePrefix") + "_"; + final Connection connection = Connections.getConnection(); + final Statement statement = connection.createStatement(); + // 修复升级程序问题 https://github.com/b3log/solo/issues/12717 + final ResultSet resultSet = statement.executeQuery("SELECT count(*) AS C FROM information_schema.COLUMNS WHERE table_name = '" + tablePrefix + "user" + "' AND column_name = 'userPassword'"); + while (resultSet.next()) { + final int c = resultSet.getInt("C"); + if (0 < c) { + final Statement drop = connection.createStatement(); + drop.executeUpdate("ALTER TABLE `" + tablePrefix + "user` DROP COLUMN `userPassword`"); + drop.close(); + } + } + resultSet.close(); + statement.close(); + connection.commit(); + connection.close(); + + final Transaction transaction = optionRepository.beginTransaction(); + + JSONObject syncGitHubOpt = optionRepository.get(Option.ID_C_SYNC_GITHUB); + if (null == syncGitHubOpt) { + syncGitHubOpt = new JSONObject(); + syncGitHubOpt.put(Keys.OBJECT_ID, Option.ID_C_SYNC_GITHUB); + syncGitHubOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + syncGitHubOpt.put(Option.OPTION_VALUE, Option.DefaultPreference.DEFAULT_SYNC_GITHUB); + optionRepository.add(syncGitHubOpt); + } + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V340_350.java b/src/main/java/org/b3log/solo/upgrade/V340_350.java new file mode 100644 index 00000000..c3d6d76a --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V340_350.java @@ -0,0 +1,109 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +/** + * Upgrade script from v3.4.0 to v3.5.0. + * + * @author Liang Ding + * @version 1.0.0.1, Mar 31, 2019 + * @since 3.5.0 + */ +public final class V340_350 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V340_350.class); + + /** + * Performs upgrade from v3.4.0 to v3.5.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.4.0"; + final String toVer = "3.5.0"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + optionRepository.remove("skinName"); + optionRepository.remove("skins"); + + JSONObject skinDirNameOpt = optionRepository.get(Option.ID_C_SKIN_DIR_NAME); + if (null == skinDirNameOpt) { + skinDirNameOpt = new JSONObject(); + skinDirNameOpt.put(Keys.OBJECT_ID, Option.ID_C_SKIN_DIR_NAME); + skinDirNameOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_SKIN); + skinDirNameOpt.put(Option.OPTION_VALUE, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + optionRepository.add(skinDirNameOpt); + } else { + skinDirNameOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_SKIN); + optionRepository.update(Option.ID_C_SKIN_DIR_NAME, skinDirNameOpt); + } + + JSONObject mobileSkinDirNameOpt = optionRepository.get(Option.ID_C_MOBILE_SKIN_DIR_NAME); + if (null == mobileSkinDirNameOpt) { + mobileSkinDirNameOpt = new JSONObject(); + mobileSkinDirNameOpt.put(Keys.OBJECT_ID, Option.ID_C_MOBILE_SKIN_DIR_NAME); + mobileSkinDirNameOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_SKIN); + mobileSkinDirNameOpt.put(Option.OPTION_VALUE, Option.DefaultPreference.DEFAULT_MOBILE_SKIN_DIR_NAME); + optionRepository.add(mobileSkinDirNameOpt); + } else { + mobileSkinDirNameOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_SKIN); + optionRepository.update(Option.ID_C_MOBILE_SKIN_DIR_NAME, mobileSkinDirNameOpt); + } + + JSONObject hljsThemeOpt = optionRepository.get(Option.ID_C_HLJS_THEME); + if (null == hljsThemeOpt) { + hljsThemeOpt = new JSONObject(); + hljsThemeOpt.put(Keys.OBJECT_ID, Option.ID_C_HLJS_THEME); + hljsThemeOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + hljsThemeOpt.put(Option.OPTION_VALUE, Option.DefaultPreference.DEFAULT_HLJS_THEME); + optionRepository.add(hljsThemeOpt); + } + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V350_360.java b/src/main/java/org/b3log/solo/upgrade/V350_360.java new file mode 100644 index 00000000..a17742bd --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V350_360.java @@ -0,0 +1,159 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.FilterOperator; +import org.b3log.latke.repository.PropertyFilter; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.latke.repository.jdbc.JdbcRepository; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.solo.model.*; +import org.b3log.solo.repository.CommentRepository; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.repository.PageRepository; +import org.b3log.solo.repository.UserRepository; +import org.b3log.solo.service.ArticleMgmtService; +import org.json.JSONObject; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.List; + +/** + * Upgrade script from v3.5.0 to v3.6.0. + * + * @author Liang Ding + * @version 1.0.0.0, Apr 18, 2019 + * @since 3.6.0 + */ +public final class V350_360 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V350_360.class); + + /** + * Performs upgrade from v3.5.0 to v3.6.0. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.5.0"; + final String toVer = "3.6.0"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + convertPagesToArticles(); + + final Transaction transaction = optionRepository.beginTransaction(); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } + + /** + * 去掉自定义页面 https://github.com/b3log/solo/issues/12764 + */ + private static void convertPagesToArticles() throws Exception { + final BeanManager beanManager = BeanManager.getInstance(); + final PageRepository pageRepository = beanManager.getReference(PageRepository.class); + final CommentRepository commentRepository = beanManager.getReference(CommentRepository.class); + final ArticleMgmtService articleMgmtService = beanManager.getReference(ArticleMgmtService.class); + final UserRepository userRepository = beanManager.getReference(UserRepository.class); + final JSONObject admin = userRepository.getAdmin(); + + final List pages = pageRepository.getList(new Query(). + setFilter(new PropertyFilter("pageType", FilterOperator.EQUAL, Page.PAGE))); + for (final JSONObject page : pages) { + final String pageId = page.optString(Keys.OBJECT_ID); + Transaction transaction = pageRepository.beginTransaction(); + pageRepository.remove(pageId); + transaction.commit(); + + final String title = page.optString(Page.PAGE_TITLE); + final String permalink = page.optString(Page.PAGE_PERMALINK); + final String content = page.optString("pageContent"); + final int commentCnt = page.optInt("pageCommentCount"); + final boolean commentable = page.optBoolean("pageCommentable"); + + final JSONObject article = new JSONObject(); + article.put(Article.ARTICLE_AUTHOR_ID, admin.optString(Keys.OBJECT_ID)); + article.put(Article.ARTICLE_TITLE, title); + article.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(content)); + article.put(Article.ARTICLE_COMMENT_COUNT, commentCnt); + if ("/my-github-repos".equals(permalink)) { + article.put(Article.ARTICLE_TAGS_REF, "开源,GitHub"); + } + article.put(Article.ARTICLE_PERMALINK, permalink); + article.put(Article.ARTICLE_COMMENTABLE, commentable); + article.put(Article.ARTICLE_CONTENT, content); + article.put(Article.ARTICLE_VIEW_PWD, ""); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, false); + + final JSONObject addArticleReq = new JSONObject(); + addArticleReq.put(Article.ARTICLE, article); + final String articleId = articleMgmtService.addArticle(addArticleReq); + final List comments = commentRepository.getList(new Query().setFilter(new PropertyFilter(Comment.COMMENT_ON_ID, FilterOperator.EQUAL, pageId))); + + transaction = pageRepository.beginTransaction(); + for (final JSONObject comment : comments) { + comment.put(Comment.COMMENT_ON_ID, articleId); + final String commentId = comment.optString(Keys.OBJECT_ID); + commentRepository.update(commentId, comment); + } + + transaction.commit(); + } + + JdbcRepository.dispose(); + + final String tablePrefix = Latkes.getLocalProperty("jdbc.tablePrefix") + "_"; + final Connection connection = Connections.getConnection(); + final Statement statement = connection.createStatement(); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "page` DROP COLUMN `pageType`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "page` DROP COLUMN `pageContent`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "page` DROP COLUMN `pageCommentCount`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "page` DROP COLUMN `pageCommentable`"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "comment` DROP COLUMN `commentOnType`"); + statement.close(); + connection.commit(); + connection.close(); + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V360_361.java b/src/main/java/org/b3log/solo/upgrade/V360_361.java new file mode 100644 index 00000000..bbc1f8f6 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V360_361.java @@ -0,0 +1,72 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +/** + * Upgrade script from v3.6.0 to v3.6.1. + * + * @author Liang Ding + * @version 1.0.0.0, May 21, 2019 + * @since 3.6.1 + */ +public final class V360_361 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V360_361.class); + + /** + * Performs upgrade from v3.6.0 to v3.6.1. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.6.0"; + final String toVer = "3.6.1"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V361_362.java b/src/main/java/org/b3log/solo/upgrade/V361_362.java new file mode 100644 index 00000000..63b1b9b7 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V361_362.java @@ -0,0 +1,117 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Comment; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.CommentRepository; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.util.Emotions; +import org.json.JSONObject; + +import java.util.List; + +/** + * Upgrade script from v3.6.1 to v3.6.2. + * + * @author Liang Ding + * @version 1.0.0.0, Jun 1, 2019 + * @since 3.6.2 + */ +public final class V361_362 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V361_362.class); + + /** + * Performs upgrade from v3.6.1 to v3.6.2. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.6.1"; + final String toVer = "3.6.2"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + final CommentRepository commentRepository = beanManager.getReference(CommentRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + // 迁移历史表情图片 https://github.com/b3log/solo/issues/12787 + final List comments = commentRepository.getList(new Query()); + for (final JSONObject comment : comments) { + final String oldContent = comment.optString(Comment.COMMENT_CONTENT); + String commentContent = oldContent; + commentContent = Emotions.convert(commentContent); + commentContent = convertEm00(commentContent); + if (!StringUtils.equalsIgnoreCase(oldContent, commentContent)) { + comment.put(Comment.COMMENT_CONTENT, commentContent); + final String commentId = comment.optString(Keys.OBJECT_ID); + comment.put(Comment.COMMENT_CONTENT, commentContent); + commentRepository.update(commentId, comment); + LOGGER.log(Level.INFO, "Migrated comment [id=" + commentId + "]'s content emoji"); + } + } + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } + + private static String convertEm00(final String content) { + String ret = StringUtils.replace(content, "[em00]", "\uD83D\uDE04"); + ret = StringUtils.replace(ret, "[em01]", "\uD83D\uDE02"); + ret = StringUtils.replace(ret, "[em02]", "\uD83D\uDE1C"); + ret = StringUtils.replace(ret, "[em03]", "\uD83D\uDE2B"); + ret = StringUtils.replace(ret, "[em04]", "\uD83D\uDE2D"); + ret = StringUtils.replace(ret, "[em05]", "\uD83D\uDE30"); + ret = StringUtils.replace(ret, "[em06]", "\uD83D\uDE21"); + ret = StringUtils.replace(ret, "[em07]", "\uD83D\uDE24"); + ret = StringUtils.replace(ret, "[em08]", "\uD83D\uDC40"); + ret = StringUtils.replace(ret, "[em09]", "\uD83D\uDE31"); + ret = StringUtils.replace(ret, "[em10]", "\uD83D\uDE0E"); + ret = StringUtils.replace(ret, "[em11]", "\uD83D\uDE0B"); + ret = StringUtils.replace(ret, "[em12]", "❤️"); + ret = StringUtils.replace(ret, "[em13]", "\uD83D\uDC94"); + ret = StringUtils.replace(ret, "[em14]", "\uD83D\uDC7F"); + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V362_363.java b/src/main/java/org/b3log/solo/upgrade/V362_363.java new file mode 100644 index 00000000..f480dfd4 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V362_363.java @@ -0,0 +1,95 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.util.Images; +import org.json.JSONObject; + +import java.util.List; + +/** + * Upgrade script from v3.6.2 to v3.6.3. + * + * @author Liang Ding + * @version 1.0.0.0, Jul 13, 2019 + * @since 3.6.3 + */ +public final class V362_363 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V362_363.class); + + /** + * Performs upgrade from v3.6.2 to v3.6.3. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.6.2"; + final String toVer = "3.6.3"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + // 提升文章首图精度 + final List articles = articleRepository.getList(new Query()); + for (final JSONObject article : articles) { + String imgURL = article.optString(Article.ARTICLE_IMG1_URL); + if (StringUtils.isBlank(imgURL)) { + imgURL = Images.imageSize(Images.randImage(), Article.ARTICLE_THUMB_IMG_WIDTH, Article.ARTICLE_THUMB_IMG_HEIGHT); + } else { + imgURL = StringUtils.replace(imgURL, "/w/768", "/w/" + Article.ARTICLE_THUMB_IMG_WIDTH); + imgURL = StringUtils.replace(imgURL, "/h/432", "/h/" + Article.ARTICLE_THUMB_IMG_HEIGHT); + } + article.put(Article.ARTICLE_IMG1_URL, imgURL); + articleRepository.update(article.optString(Keys.OBJECT_ID), article); + } + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V363_364.java b/src/main/java/org/b3log/solo/upgrade/V363_364.java new file mode 100644 index 00000000..2d2d4802 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V363_364.java @@ -0,0 +1,89 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.ArticleRepository; +import org.b3log.solo.repository.OptionRepository; +import org.b3log.solo.util.Images; +import org.json.JSONObject; + +import java.util.List; + +/** + * Upgrade script from v3.6.3 to v3.6.4. + * + * @author Liang Ding + * @version 1.0.0.0, Aug 18, 2019 + * @since 3.6.4 + */ +public final class V363_364 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V363_364.class); + + /** + * Performs upgrade from v3.6.3 to v3.6.4. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.6.3"; + final String toVer = "3.6.4"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + JSONObject pullGitHubOpt = optionRepository.get(Option.ID_C_PULL_GITHUB); + if (null == pullGitHubOpt) { + pullGitHubOpt = new JSONObject(); + pullGitHubOpt.put(Keys.OBJECT_ID, Option.ID_C_PULL_GITHUB); + pullGitHubOpt.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + pullGitHubOpt.put(Option.OPTION_VALUE, Option.DefaultPreference.DEFAULT_PULL_GITHUB); + optionRepository.add(pullGitHubOpt); + } + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/V364_365.java b/src/main/java/org/b3log/solo/upgrade/V364_365.java new file mode 100644 index 00000000..f26c9ff6 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/V364_365.java @@ -0,0 +1,76 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.upgrade; + +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.model.Option; +import org.b3log.solo.repository.OptionRepository; +import org.json.JSONObject; + +/** + * Upgrade script from v3.6.4 to v3.6.5. + * + * @author Liang Ding + * @version 1.0.0.0, Sep 18, 2019 + * @since 3.6.5 + */ +public final class V364_365 { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(V364_365.class); + + /** + * Performs upgrade from v3.6.4 to v3.6.5. + * + * @throws Exception upgrade fails + */ + public static void perform() throws Exception { + final String fromVer = "3.6.4"; + final String toVer = "3.6.5"; + + LOGGER.log(Level.INFO, "Upgrading from version [" + fromVer + "] to version [" + toVer + "]...."); + + final BeanManager beanManager = BeanManager.getInstance(); + final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class); + + try { + final Transaction transaction = optionRepository.beginTransaction(); + + final JSONObject versionOpt = optionRepository.get(Option.ID_C_VERSION); + versionOpt.put(Option.OPTION_VALUE, toVer); + optionRepository.update(Option.ID_C_VERSION, versionOpt); + + JSONObject hljsOpt = optionRepository.get(Option.ID_C_HLJS_THEME); + hljsOpt.put(Option.OPTION_VALUE, "github"); + optionRepository.update(Option.ID_C_HLJS_THEME, hljsOpt); + + transaction.commit(); + + LOGGER.log(Level.INFO, "Upgraded from version [" + fromVer + "] to version [" + toVer + "] successfully"); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Upgrade failed!", e); + + throw new Exception("Upgrade failed from version [" + fromVer + "] to version [" + toVer + "]"); + } + } +} diff --git a/src/main/java/org/b3log/solo/upgrade/package-info.java b/src/main/java/org/b3log/solo/upgrade/package-info.java new file mode 100644 index 00000000..824c3b51 --- /dev/null +++ b/src/main/java/org/b3log/solo/upgrade/package-info.java @@ -0,0 +1,4 @@ +/** + * Upgrade scripts. + */ +package org.b3log.solo.upgrade; diff --git a/src/main/java/org/b3log/solo/util/Emotions.java b/src/main/java/org/b3log/solo/util/Emotions.java new file mode 100644 index 00000000..0220c5b8 --- /dev/null +++ b/src/main/java/org/b3log/solo/util/Emotions.java @@ -0,0 +1,92 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import com.vdurmont.emoji.EmojiParser; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Latkes; +import org.b3log.solo.SoloServletListener; + +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Emotions utilities. + * + * @author Liang Ding + * @author Vanessa + * @version 1.1.1.1, Sep 11, 2019 + * @since 1.4.0 + */ +public final class Emotions { + + /** + * Emoji pattern. + */ + private static final Pattern EMOJI_PATTERN = Pattern.compile(":.+:"); + + /** + * Converts the specified content with emotions. + * + * @param content the specified content + * @return converted content + */ + public static String convert(final String content) { + String ret = content; + if (!EMOJI_PATTERN.matcher(ret).find()) { + return ret; + } + + ret = toUnicode(ret); + for (final String emojiCode : EMOJIS) { + String repl = "\"""; + ret = ret.replace(":" + emojiCode + ":", repl); + } + + return ret; + } + + /** + * Replaces the emoji's alias by its unicode. Example: ":smile:" gives "😄". + * + * @param content the specified string to parse + * @return the string with the mojis replaces by their unicode + */ + private static String toUnicode(final String content) { + String ret = EmojiParser.parseToUnicode(content); + ret = ret.replace("❤", "❤️"); + ret = ret.replace("♥", "❤️"); + + return ret; + } + + /** + * Emoji list. + */ + private static final String[] EMOJIS = { + "100", "1234", "+1", "-1", "1st_place_medal", "2nd_place_medal", "3rd_place_medal", "8ball", "a", "ab", "abc", "abcd", "accept", "aerial_tramway", "afghanistan", "airplane", "aland_islands", "alarm_clock", "albania", "alembic", "algeria", "alien", "ambulance", "american_samoa", "amphora", "anchor", "andorra", "angel", "anger", "angola", "angry", "anguilla", "anguished", "ant", "antarctica", "antigua_barbuda", "apple", "aquarius", "argentina", "aries", "armenia", "arrow_backward", "arrow_double_down", "arrow_double_up", "arrow_down", "arrow_down_small", "arrow_forward", "arrow_heading_down", "arrow_heading_up", "arrow_left", "arrow_lower_left", "arrow_lower_right", "arrow_right", "arrow_right_hook", "arrow_up", "arrow_up_down", "arrow_up_small", "arrow_upper_left", "arrow_upper_right", "arrows_clockwise", "arrows_counterclockwise", "art", "articulated_lorry", "artificial_satellite", "aruba", "asterisk", "astonished", "athletic_shoe", "atm", "atom_symbol", "australia", "austria", "avocado", "azerbaijan", "b", "b3log", "baby", "baby_bottle", "baby_chick", "baby_symbol", "back", "bacon", "badminton", "baggage_claim", "baguette_bread", "bahamas", "bahrain", "balance_scale", "balloon", "ballot_box", "ballot_box_with_check", "bamboo", "banana", "bangbang", "bangladesh", "bank", "bar_chart", "barbados", "barber", "baseball", "basketball", "basketball_man", "basketball_woman", "bat", "bath", "bathtub", "battery", "beach_umbrella", "bear", "bed", "bee", "beer", "beers", "beetle", "beginner", "belarus", "belgium", "belize", "bell", "bellhop_bell", "benin", "bento", "bermuda", "bhutan", "bicyclist", "bike", "biking_man", "biking_woman", "bikini", "biohazard", "bird", "birthday", "black_circle", "black_flag", "black_heart", "black_joker", "black_large_square", "black_medium_small_square", "black_medium_square", "black_nib", "black_small_square", "black_square_button", "blonde_man", "blonde_woman", "blossom", "blowfish", "blue_book", "blue_car", "blue_heart", "blush", "boar", "boat", "bolivia", "bomb", "book", "bookmark", "bookmark_tabs", "books", "boom", "boot", "bosnia_herzegovina", "botswana", "bouquet", "bow", "bow_and_arrow", "bowing_man", "bowing_woman", "bowling", "boxing_glove", "boy", "brazil", "bread", "bride_with_veil", "bridge_at_night", "briefcase", "british_indian_ocean_territory", "british_virgin_islands", "broken_heart", "brunei", "bug", "building_construction", "bulb", "bulgaria", "bullettrain_front", "bullettrain_side", "burkina_faso", "burrito", "burundi", "bus", "business_suit_levitating", "busstop", "bust_in_silhouette", "busts_in_silhouette", "butterfly", "cactus", "cake", "calendar", "call_me_hand", "calling", "cambodia", "camel", "camera", "camera_flash", "cameroon", "camping", "canada", "canary_islands", "cancer", "candle", "candy", "canoe", "cape_verde", "capital_abcd", "capricorn", "car", "card_file_box", "card_index", "card_index_dividers", "caribbean_netherlands", "carousel_horse", "carrot", "cat", "cat2", "cayman_islands", "cd", "central_african_republic", "chad", "chainbook", "chains", "champagne", "chart", "chart_with_downwards_trend", "chart_with_upwards_trend", "checkered_flag", "cheese", "cherries", "cherry_blossom", "chestnut", "chicken", "children_crossing", "chile", "chipmunk", "chocolate_bar", "christmas_island", "christmas_tree", "church", "cinema", "circus_tent", "city_sunrise", "city_sunset", "cityscape", "cl", "clamp", "clap", "clapper", "classical_building", "clinking_glasses", "clipboard", "clock1", "clock10", "clock1030", "clock11", "clock1130", "clock12", "clock1230", "clock130", "clock2", "clock230", "clock3", "clock330", "clock4", "clock430", "clock5", "clock530", "clock6", "clock630", "clock7", "clock730", "clock8", "clock830", "clock9", "clock930", "closed_book", "closed_lock_with_key", "closed_umbrella", "cloud", "cloud_with_lightning", "cloud_with_lightning_and_rain", "cloud_with_rain", "cloud_with_snow", "clown_face", "clubs", "cn", "cocktail", "cocos_islands", "coffee", "coffin", "cold_sweat", "collision", "colombia", "comet", "comoros", "computer", "computer_mouse", "confetti_ball", "confounded", "confused", "congo_brazzaville", "congo_kinshasa", "congratulations", "construction", "construction_worker", "construction_worker_man", "construction_worker_woman", "control_knobs", "convenience_store", "cook_islands", "cookie", "cool", "cop", "copyright", "corn", "costa_rica", "cote_divoire", "couch_and_lamp", "couple", "couple_with_heart", "couple_with_heart_man_man", "couple_with_heart_woman_man", "couple_with_heart_woman_woman", "couplekiss_man_man", "couplekiss_man_woman", "couplekiss_woman_woman", "cow", "cow2", "cowboy_hat_face", "crab", "crayon", "credit_card", "crescent_moon", "cricket", "croatia", "crocodile", "croissant", "crossed_fingers", "crossed_flags", "crossed_swords", "crown", "cry", "crying_cat_face", "crystal_ball", "cuba", "cucumber", "cupid", "curacao", "curly_loop", "currency_exchange", "curry", "custard", "customs", "cyclone", "cyprus", "czech_republic", "dagger", "dancer", "dancers", "dancing_men", "dancing_women", "dango", "dark_sunglasses", "dart", "dash", "date", "de", "deciduous_tree", "deer", "denmark", "department_store", "derelict_house", "desert", "desert_island", "desktop_computer", "detective", "diamond_shape_with_a_dot_inside", "diamonds", "disappointed", "disappointed_relieved", "dizzy", "dizzy_face", "djibouti", "do_not_litter", "dog", "dog2", "doge", "dollar", "dolls", "dolphin", "dominica", "dominican_republic", "door", "doughnut", "dove", "dragon", "dragon_face", "dress", "dromedary_camel", "drooling_face", "droplet", "drum", "duck", "dvd", "e-mail", "eagle", "ear", "ear_of_rice", "earth_africa", "earth_americas", "earth_asia", "ecuador", "egg", "eggplant", "egypt", "eight", "eight_pointed_black_star", "eight_spoked_asterisk", "el_salvador", "electric_plug", "elephant", "email", "end", "envelope", "envelope_with_arrow", "equatorial_guinea", "eritrea", "es", "estonia", "ethiopia", "eu", "euro", "european_castle", "european_post_office", "european_union", "evergreen_tree", "exclamation", "expressionless", "eye", "eye_speech_bubble", "eyeglasses", "eyes", "face_with_head_bandage", "face_with_thermometer", "facepunch", "factory", "falkland_islands", "fallen_leaf", "family", "family_man_boy", "family_man_boy_boy", "family_man_girl", "family_man_girl_boy", "family_man_girl_girl", "family_man_man_boy", "family_man_man_boy_boy", "family_man_man_girl", "family_man_man_girl_boy", "family_man_man_girl_girl", "family_man_woman_boy", "family_man_woman_boy_boy", "family_man_woman_girl", "family_man_woman_girl_boy", "family_man_woman_girl_girl", "family_woman_boy", "family_woman_boy_boy", "family_woman_girl", "family_woman_girl_boy", "family_woman_girl_girl", "family_woman_woman_boy", "family_woman_woman_boy_boy", "family_woman_woman_girl", "family_woman_woman_girl_boy", "family_woman_woman_girl_girl", "faroe_islands", "fast_forward", "fax", "fearful", "feet", "female_detective", "ferris_wheel", "ferry", "field_hockey", "fiji", "file_cabinet", "file_folder", "film_projector", "film_strip", "finland", "fire", "fire_engine", "fireworks", "first_quarter_moon", "first_quarter_moon_with_face", "fish", "fish_cake", "fishing_pole_and_fish", "fist", "fist_left", "fist_oncoming", "fist_raised", "fist_right", "five", "flags", "flashlight", "fleur_de_lis", "flight_arrival", "flight_departure", "flipper", "floppy_disk", "flower_playing_cards", "flushed", "fog", "foggy", "football", "footprints", "fork_and_knife", "fountain", "fountain_pen", "four", "four_leaf_clover", "fox_face", "fr", "framed_picture", "free", "french_guiana", "french_polynesia", "french_southern_territories", "fried_egg", "fried_shrimp", "fries", "frog", "frowning", "frowning_face", "frowning_man", "frowning_woman", "fu", "fuelpump", "full_moon", "full_moon_with_face", "funeral_urn", "gabon", "gambia", "game_die", "gb", "gear", "gem", "gemini", "georgia", "ghana", "ghost", "gibraltar", "gift", "gift_heart", "girl", "globe_with_meridians", "goal_net", "goat", "golf", "golfing_man", "golfing_woman", "gorilla", "grapes", "greece", "green_apple", "green_book", "green_heart", "green_salad", "greenland", "grenada", "grey_exclamation", "grey_question", "grimacing", "grin", "grinning", "guadeloupe", "guam", "guardsman", "guardswoman", "guatemala", "guernsey", "guinea", "guinea_bissau", "guitar", "gun", "guyana", "hacpai", "haircut", "haircut_man", "haircut_woman", "haiti", "hamburger", "hammer", "hammer_and_pick", "hammer_and_wrench", "hamster", "hand", "handbag", "handshake", "hankey", "hash", "hatched_chick", "hatching_chick", "headphones", "hear_no_evil", "heart", "heart_decoration", "heart_eyes", "heart_eyes_cat", "heartbeat", "heartpulse", "hearts", "heavy_check_mark", "heavy_division_sign", "heavy_dollar_sign", "heavy_exclamation_mark", "heavy_heart_exclamation", "heavy_minus_sign", "heavy_multiplication_x", "heavy_plus_sign", "helicopter", "herb", "hibiscus", "high_brightness", "high_heel", "hocho", "hole", "honduras", "honey_pot", "honeybee", "hong_kong", "horse", "horse_racing", "hospital", "hot_pepper", "hotdog", "hotel", "hotsprings", "hourglass", "hourglass_flowing_sand", "house", "house_with_garden", "houses", "huaji", "hugs", "hungary", "hushed", "ice_cream", "ice_hockey", "ice_skate", "icecream", "iceland", "id", "ideograph_advantage", "imp", "inbox_tray", "incoming_envelope", "india", "indonesia", "information_desk_person", "information_source", "innocent", "interrobang", "iphone", "iran", "iraq", "ireland", "isle_of_man", "israel", "it", "izakaya_lantern", "jack_o_lantern", "jamaica", "japan", "japanese_castle", "japanese_goblin", "japanese_ogre", "jeans", "jersey", "jordan", "joy", "joy_cat", "joystick", "jp", "kaaba", "kazakhstan", "kenya", "key", "keyboard", "keycap_ten", "kick_scooter", "kimono", "kiribati", "kiss", "kissing", "kissing_cat", "kissing_closed_eyes", "kissing_heart", "kissing_smiling_eyes", "kiwi_fruit", "knife", "koala", "koko", "kosovo", "kr", "kuwait", "kyrgyzstan", "label", "lantern", "laos", "large_blue_circle", "large_blue_diamond", "large_orange_diamond", "last_quarter_moon", "last_quarter_moon_with_face", "latin_cross", "latke", "latvia", "laughing", "leaves", "lebanon", "ledger", "left_luggage", "left_right_arrow", "leftwards_arrow_with_hook", "lemon", "leo", "leopard", "lesotho", "level_slider", "liberia", "libra", "libya", "liechtenstein", "light_rail", "link", "lion", "lips", "lipstick", "lithuania", "lizard", "lock", "lock_with_ink_pen", "lollipop", "loop", "loud_sound", "loudspeaker", "love_hotel", "love_letter", "low_brightness", "luxembourg", "lying_face", "m", "macau", "macedonia", "madagascar", "mag", "mag_right", "mahjong", "mailbox", "mailbox_closed", "mailbox_with_mail", "mailbox_with_no_mail", "malawi", "malaysia", "maldives", "male_detective", "mali", "malta", "man", "man_artist", "man_astronaut", "man_cartwheeling", "man_cook", "man_dancing", "man_facepalming", "man_factory_worker", "man_farmer", "man_firefighter", "man_health_worker", "man_in_tuxedo", "man_judge", "man_juggling", "man_mechanic", "man_office_worker", "man_pilot", "man_playing_handball", "man_playing_water_polo", "man_scientist", "man_shrugging", "man_singer", "man_student", "man_teacher", "man_technologist", "man_with_gua_pi_mao", "man_with_turban", "mandarin", "mans_shoe", "mantelpiece_clock", "maple_leaf", "marshall_islands", "martial_arts_uniform", "martinique", "mask", "massage", "massage_man", "massage_woman", "mauritania", "mauritius", "mayotte", "meat_on_bone", "medal_military", "medal_sports", "mega", "melon", "memo", "men_wrestling", "menorah", "mens", "metal", "metro", "mexico", "micronesia", "microphone", "microscope", "middle_finger", "milk_glass", "milky_way", "minibus", "minidisc", "mobile_phone_off", "moldova", "monaco", "money_mouth_face", "money_with_wings", "moneybag", "mongolia", "monkey", "monkey_face", "monorail", "montenegro", "montserrat", "moon", "morocco", "mortar_board", "mosque", "motor_boat", "motor_scooter", "motorcycle", "motorway", "mount_fuji", "mountain", "mountain_bicyclist", "mountain_biking_man", "mountain_biking_woman", "mountain_cableway", "mountain_railway", "mountain_snow", "mouse", "mouse2", "movie_camera", "moyai", "mozambique", "mrs_claus", "muscle", "mushroom", "musical_keyboard", "musical_note", "musical_score", "mute", "myanmar", "nail_care", "name_badge", "namibia", "national_park", "nauru", "nauseated_face", "necktie", "negative_squared_cross_mark", "nepal", "nerd_face", "netherlands", "neutral_face", "new", "new_caledonia", "new_moon", "new_moon_with_face", "new_zealand", "newspaper", "newspaper_roll", "next_track_button", "ng", "ng_man", "ng_woman", "nicaragua", "niger", "nigeria", "night_with_stars", "nine", "niue", "no_bell", "no_bicycles", "no_entry", "no_entry_sign", "no_good", "no_good_man", "no_good_woman", "no_mobile_phones", "no_mouth", "no_pedestrians", "no_smoking", "non-potable_water", "norfolk_island", "north_korea", "northern_mariana_islands", "norway", "nose", "notebook", "notebook_with_decorative_cover", "notes", "nut_and_bolt", "o", "o2", "ocean", "octocat", "octopus", "oden", "office", "oil_drum", "ok", "ok_hand", "ok_man", "ok_woman", "old_key", "older_man", "older_woman", "om", "oman", "on", "oncoming_automobile", "oncoming_bus", "oncoming_police_car", "oncoming_taxi", "one", "open_book", "open_file_folder", "open_hands", "open_mouth", "open_umbrella", "ophiuchus", "orange", "orange_book", "orthodox_cross", "outbox_tray", "owl", "ox", "package", "page_facing_up", "page_with_curl", "pager", "paintbrush", "pakistan", "palau", "palestinian_territories", "palm_tree", "panama", "pancakes", "panda_face", "paperclip", "paperclips", "papua_new_guinea", "paraguay", "parasol_on_ground", "parking", "part_alternation_mark", "partly_sunny", "passenger_ship", "passport_control", "pause_button", "paw_prints", "peace_symbol", "peach", "peanuts", "pear", "pen", "pencil", "pencil2", "penguin", "pensive", "performing_arts", "persevere", "person_fencing", "person_frowning", "person_with_blond_hair", "person_with_pouting_face", "peru", "philippines", "phone", "pick", "pig", "pig2", "pig_nose", "pill", "pineapple", "ping_pong", "pipe", "pisces", "pitcairn_islands", "pizza", "place_of_worship", "plate_with_cutlery", "play_or_pause_button", "point_down", "point_left", "point_right", "point_up", "point_up_2", "poland", "police_car", "policeman", "policewoman", "poodle", "poop", "popcorn", "portugal", "post_office", "postal_horn", "postbox", "potable_water", "potato", "pouch", "poultry_leg", "pound", "pout", "pouting_cat", "pouting_man", "pouting_woman", "pray", "prayer_beads", "pregnant_woman", "previous_track_button", "prince", "princess", "printer", "puerto_rico", "punch", "purple_heart", "purse", "pushpin", "put_litter_in_its_place", "qatar", "question", "rabbit", "rabbit2", "racehorse", "racing_car", "radio", "radio_button", "radioactive", "rage", "railway_car", "railway_track", "rainbow", "rainbow_flag", "raised_back_of_hand", "raised_hand", "raised_hand_with_fingers_splayed", "raised_hands", "raising_hand", "raising_hand_man", "raising_hand_woman", "ram", "ramen", "rat", "record_button", "recycle", "red_car", "red_circle", "registered", "relaxed", "relieved", "reminder_ribbon", "repeat", "repeat_one", "rescue_worker_helmet", "restroom", "reunion", "revolving_hearts", "rewind", "rhinoceros", "ribbon", "rice", "rice_ball", "rice_cracker", "rice_scene", "right_anger_bubble", "ring", "robot", "rocket", "rofl", "roll_eyes", "roller_coaster", "romania", "rooster", "rose", "rosette", "rotating_light", "round_pushpin", "rowboat", "rowing_man", "rowing_woman", "ru", "rugby_football", "runner", "running", "running_man", "running_shirt_with_sash", "running_woman", "rwanda", "sa", "sagittarius", "sailboat", "sake", "samoa", "san_marino", "sandal", "santa", "sao_tome_principe", "sassy_man", "sassy_woman", "satellite", "satisfied", "saudi_arabia", "saxophone", "school", "school_satchel", "scissors", "scorpion", "scorpius", "scream", "scream_cat", "scroll", "seat", "secret", "see_no_evil", "seedling", "selfie", "senegal", "serbia", "seven", "seychelles", "shallow_pan_of_food", "shamrock", "shark", "shaved_ice", "sheep", "shell", "shield", "shinto_shrine", "ship", "shirt", "shit", "shoe", "shopping", "shopping_cart", "shower", "shrimp", "sierra_leone", "signal_strength", "singapore", "sint_maarten", "six", "six_pointed_star", "ski", "skier", "skull", "skull_and_crossbones", "sleeping", "sleeping_bed", "sleepy", "slightly_frowning_face", "slightly_smiling_face", "slot_machine", "slovakia", "slovenia", "small_airplane", "small_blue_diamond", "small_orange_diamond", "small_red_triangle", "small_red_triangle_down", "smile", "smile_cat", "smiley", "smiley_cat", "smiling_imp", "smirk", "smirk_cat", "smoking", "snail", "snake", "sneezing_face", "snowboarder", "snowflake", "snowman", "snowman_with_snow", "sob", "soccer", "solo", "solomon_islands", "somalia", "soon", "sos", "sound", "south_africa", "south_georgia_south_sandwich_islands", "south_sudan", "space_invader", "spades", "spaghetti", "sparkle", "sparkler", "sparkles", "sparkling_heart", "speak_no_evil", "speaker", "speaking_head", "speech_balloon", "speedboat", "spider", "spider_web", "spiral_calendar", "spiral_notepad", "spoon", "squid", "sri_lanka", "st_barthelemy", "st_helena", "st_kitts_nevis", "st_lucia", "st_pierre_miquelon", "st_vincent_grenadines", "stadium", "star", "star2", "star_and_crescent", "star_of_david", "stars", "station", "statue_of_liberty", "steam_locomotive", "stew", "stop_button", "stop_sign", "stopwatch", "straight_ruler", "strawberry", "stuck_out_tongue", "stuck_out_tongue_closed_eyes", "stuck_out_tongue_winking_eye", "studio_microphone", "stuffed_flatbread", "sudan", "sun_behind_large_cloud", "sun_behind_rain_cloud", "sun_behind_small_cloud", "sun_with_face", "sunflower", "sunglasses", "sunny", "sunrise", "sunrise_over_mountains", "surfer", "surfing_man", "surfing_woman", "suriname", "sushi", "suspension_railway", "swaziland", "sweat", "sweat_drops", "sweat_smile", "sweden", "sweet_potato", "swimmer", "swimming_man", "swimming_woman", "switzerland", "sym", "symbols", "synagogue", "syria", "syringe", "taco", "tada", "taiwan", "tajikistan", "tanabata_tree", "tangerine", "tanzania", "taurus", "taxi", "tea", "telephone", "telephone_receiver", "telescope", "tennis", "tent", "thailand", "thermometer", "thinking", "thought_balloon", "three", "thumbsdown", "thumbsup", "ticket", "tickets", "tiger", "tiger2", "timer_clock", "timor_leste", "tipping_hand_man", "tipping_hand_woman", "tired_face", "tm", "togo", "toilet", "tokelau", "tokyo_tower", "tomato", "tonga", "tongue", "top", "tophat", "tornado", "tr", "trackball", "tractor", "traffic_light", "train", "train2", "tram", "triangular_flag_on_post", "triangular_ruler", "trident", "trinidad_tobago", "triumph", "trolleybus", "trollface", "trophy", "tropical_drink", "tropical_fish", "truck", "trumpet", "tshirt", "tulip", "tumbler_glass", "tunisia", "turkey", "turkmenistan", "turks_caicos_islands", "turtle", "tuvalu", "tv", "twisted_rightwards_arrows", "two", "two_hearts", "two_men_holding_hands", "two_women_holding_hands", "u5272", "u5408", "u55b6", "u6307", "u6708", "u6709", "u6e80", "u7121", "u7533", "u7981", "u7a7a", "uganda", "uk", "ukraine", "umbrella", "unamused", "underage", "unicorn", "united_arab_emirates", "unlock", "up", "upside_down_face", "uruguay", "us", "us_virgin_islands", "uzbekistan", "v", "vanuatu", "vatican_city", "vditor", "venezuela", "vertical_traffic_light", "vhs", "vibration_mode", "video_camera", "video_game", "vietnam", "violin", "virgo", "volcano", "volleyball", "vs", "vulcan_salute", "walking", "walking_man", "walking_woman", "wallis_futuna", "waning_crescent_moon", "waning_gibbous_moon", "warning", "wastebasket", "watch", "water_buffalo", "watermelon", "wave", "wavy_dash", "waxing_crescent_moon", "waxing_gibbous_moon", "wc", "weary", "wedding", "weight_lifting_man", "weight_lifting_woman", "western_sahara", "whale", "whale2", "wheel_of_dharma", "wheelchair", "white_check_mark", "white_circle", "white_flag", "white_flower", "white_large_square", "white_medium_small_square", "white_medium_square", "white_small_square", "white_square_button", "wide", "wilted_flower", "wind_chime", "wind_face", "wine_glass", "wink", "wolf", "woman", "woman_artist", "woman_astronaut", "woman_cartwheeling", "woman_cook", "woman_facepalming", "woman_factory_worker", "woman_farmer", "woman_firefighter", "woman_health_worker", "woman_judge", "woman_juggling", "woman_mechanic", "woman_office_worker", "woman_pilot", "woman_playing_handball", "woman_playing_water_polo", "woman_scientist", "woman_shrugging", "woman_singer", "woman_student", "woman_teacher", "woman_technologist", "woman_with_turban", "womans_clothes", "womans_hat", "women_wrestling", "womens", "world_map", "worried", "wrench", "writing_hand", "x", "yellow_heart", "yemen", "yen", "yin_yang", "yum", "zambia", "zap", "zero", "zimbabwe", "zipper_mouth_face", "zzz" + }; + + /** + * Private constructor. + */ + private Emotions() { + } +} diff --git a/src/main/java/org/b3log/solo/util/GitHubs.java b/src/main/java/org/b3log/solo/util/GitHubs.java new file mode 100644 index 00000000..c256729d --- /dev/null +++ b/src/main/java/org/b3log/solo/util/GitHubs.java @@ -0,0 +1,124 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.User; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.UserExt; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletResponse; + +/** + * GitHub utilities. + * + * @author Liang Ding + * @version 1.0.0.0, Feb 8, 2019 + * @since 3.0.0 + */ +public final class GitHubs { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(GitHubs.class); + + /** + * Gets GitHub repos. + * + * @param githubUserId the specified GitHub user id + * @return GitHub repos, returns {@code null} if not found + */ + public static JSONArray getGitHubRepos(final String githubUserId) { + try { + final HttpResponse res = HttpRequest.get("https://hacpai.com/github/repos?id=" + githubUserId).trustAllCerts(true). + connectionTimeout(3000).timeout(7000).header("User-Agent", Solos.USER_AGENT).send(); + if (HttpServletResponse.SC_OK != res.statusCode()) { + return null; + } + res.charset("UTF-8"); + final JSONObject result = new JSONObject(res.bodyText()); + if (0 != result.optInt(Keys.STATUS_CODE)) { + return null; + } + final JSONObject data = result.optJSONObject(Common.DATA); + final JSONArray ret = data.optJSONArray("githubrepos"); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets GitHub repos failed", e); + + return null; + } + } + + /** + * Gets GitHub user info. + * + * @param accessToken the specified access token + * @return GitHub user info, for example,
+     * {
+     *   "openId": "",
+     *   "userName": "D",
+     *   "userAvatar": "https://avatars3.githubusercontent.com/u/873584?v=4"
+     * }
+     * 
, returns {@code null} if not found QQ user info + */ + public static JSONObject getGitHubUserInfo(final String accessToken) { + try { + final HttpResponse res = HttpRequest.get("https://hacpai.com/github/user?ak=" + accessToken).trustAllCerts(true). + connectionTimeout(3000).timeout(7000).header("User-Agent", Solos.USER_AGENT).send(); + if (HttpServletResponse.SC_OK != res.statusCode()) { + return null; + } + res.charset("UTF-8"); + final JSONObject result = new JSONObject(res.bodyText()); + if (0 != result.optInt(Keys.STATUS_CODE)) { + return null; + } + final JSONObject data = result.optJSONObject(Common.DATA); + final String userName = StringUtils.trim(data.optString("userName")); + final String openId = data.optString("userId"); + final String avatarUrl = data.optString("userAvatarURL"); + + final JSONObject ret = new JSONObject(); + ret.put("openId", openId); + ret.put(User.USER_NAME, userName); + ret.put(UserExt.USER_AVATAR, avatarUrl); + + return ret; + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets GitHub user info failed", e); + + return null; + } + } + + /** + * Private constructor. + */ + private GitHubs() { + } +} diff --git a/src/main/java/org/b3log/solo/util/Images.java b/src/main/java/org/b3log/solo/util/Images.java new file mode 100644 index 00000000..2a9c8da3 --- /dev/null +++ b/src/main/java/org/b3log/solo/util/Images.java @@ -0,0 +1,137 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.apache.commons.lang.time.DateUtils; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Image utilities. + * + * @author Liang Ding + * @version 1.1.0.1, Apr 13, 2019 + * @since 2.7.0 + */ +public final class Images { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(Images.class); + + /** + * Qiniu image processing. + * + * @param html the specified content HTML + * @return processed content + */ + public static String qiniuImgProcessing(final String html) { + String ret = html; + final String qiniuDomain = "https://img.hacpai.com"; + final String[] imgSrcs = StringUtils.substringsBetween(html, " randomImages(final int n) { + final List ret = new ArrayList<>(); + + int i = 0; + while (i < n * 5) { + final String url = randImage(); + if (!ret.contains(url)) { + ret.add(url); + } + + if (ret.size() >= n) { + return ret; + } + + i++; + } + + return ret; + } + + /** + * Private constructor. + */ + private Images() { + } +} diff --git a/src/main/java/org/b3log/solo/util/Markdowns.java b/src/main/java/org/b3log/solo/util/Markdowns.java new file mode 100644 index 00000000..65f5d0c4 --- /dev/null +++ b/src/main/java/org/b3log/solo/util/Markdowns.java @@ -0,0 +1,333 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import com.vladsch.flexmark.ext.autolink.AutolinkExtension; +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; +import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension; +import com.vladsch.flexmark.ext.tables.TablesExtension; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.MutableDataSet; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.util.Callstacks; +import org.b3log.latke.util.Stopwatchs; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.parser.Parser; +import org.jsoup.safety.Whitelist; +import org.jsoup.select.NodeVisitor; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; +import java.util.concurrent.*; + +/** + * Markdown utilities. + *

+ * Uses the markdown-http as the processor, if not found this command, try + * built-in flexmark instead. + *

+ * + * @author Liang Ding + * @version 2.3.1.9, Sep 17, 2019 + * @since 0.4.5 + */ +public final class Markdowns { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(Markdowns.class); + + /** + * Markdown cache. + */ + private static final Map MD_CACHE = new ConcurrentHashMap<>(); + + /** + * Markdown to HTML timeout. + */ + private static final int MD_TIMEOUT = 10000; + + /** + * Built-in MD engine options. + */ + private static final DataHolder OPTIONS = new MutableDataSet(). + set(com.vladsch.flexmark.parser.Parser.EXTENSIONS, Arrays.asList( + TablesExtension.create(), + TaskListExtension.create(), + StrikethroughExtension.create(), + AutolinkExtension.create())). + set(HtmlRenderer.SOFT_BREAK, "
\n"); + + /** + * Built-in MD engine parser. + */ + private static final com.vladsch.flexmark.parser.Parser PARSER = + com.vladsch.flexmark.parser.Parser.builder(OPTIONS).build(); + + /** + * Built-in MD engine HTML renderer. + */ + private static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS).build(); + + /** + * Lute engine serve path. https://github.com/b3log/lute + */ + private static final String LUTE_ENGINE_URL = "http://localhost:8249"; + + /** + * Whether Lute is available. + */ + public static boolean LUTE_AVAILABLE; + + static { + try { + final String html = toHtmlByLute("旧日的足迹"); + LUTE_AVAILABLE = StringUtils.contains(html, "

旧日的足迹

"); + if (LUTE_AVAILABLE) { + LOGGER.log(Level.INFO, "[Lute] is available"); + } + } catch (final Exception e) { + // ignored + } + } + + /** + * Cleans the specified HTML. + * + * @param html the specified HTML + * @return html + */ + public static String clean(final String html) { + final Whitelist whitelist = Whitelist.relaxed(); + // 允许代码块语言高亮信息 + whitelist.addAttributes("pre", "class"). + addAttributes("div", "class"). + addAttributes("span", "class"). + addAttributes("code", "class"); + return Jsoup.clean(html, whitelist); + } + + /** + * Converts the specified markdown text to HTML. + * + * @param markdownText the specified markdown text + * @return converted HTML, returns an empty string "" if the specified markdown text is "" or {@code null}, returns + * 'markdownErrorLabel' if exception + */ + public static String toHTML(final String markdownText) { + if (StringUtils.isBlank(markdownText)) { + return ""; + } + + final String cachedHTML = getHTML(markdownText); + if (null != cachedHTML) { + return cachedHTML; + } + + final LangPropsService langPropsService = BeanManager.getInstance().getReference(LangPropsService.class); + + final ExecutorService pool = Executors.newSingleThreadExecutor(); + final long[] threadId = new long[1]; + + final Callable call = () -> { + threadId[0] = Thread.currentThread().getId(); + + String html = null; + if (LUTE_AVAILABLE) { + try { + html = toHtmlByLute(markdownText); + } catch (final Exception e) { + LOGGER.log(Level.WARN, "Failed to use [Lute] for markdown [md=" + StringUtils.substring(markdownText, 0, 256) + "]: " + e.getMessage()); + } + } + + if (StringUtils.isBlank(html)) { + html = toHtmlByFlexmark(markdownText); + } + + if (!StringUtils.startsWith(html, "

")) { + html = "

" + html + "

"; + } + + final Document doc = Jsoup.parse(html); + doc.select("a").forEach(a -> { + final String src = a.attr("href"); + if (!StringUtils.startsWithIgnoreCase(src, Latkes.getServePath()) && !StringUtils.startsWithIgnoreCase(src, "#")) { + a.attr("target", "_blank"); + } + a.removeAttr("id"); + }); + + + final List toRemove = new ArrayList<>(); + doc.traverse(new NodeVisitor() { + @Override + public void head(final org.jsoup.nodes.Node node, int depth) { + if (node instanceof org.jsoup.nodes.TextNode) { + final org.jsoup.nodes.TextNode textNode = (org.jsoup.nodes.TextNode) node; + final org.jsoup.nodes.Node parent = textNode.parent(); + + if (parent instanceof Element) { + final Element parentElem = (Element) parent; + if (parentElem.tagName().equals("code") || parentElem.tagName().equals("pre")) { + return; + } + + if (parentElem.tagName().equals("span") && StringUtils.startsWithIgnoreCase(parentElem.attr("class"), "hljs")) { + return; + } + + String text = textNode.getWholeText(); + text = Emotions.convert(text); + if (text.contains("@ nodes = Parser.parseFragment(text, parentElem, ""); + final int index = textNode.siblingIndex(); + parentElem.insertChildren(index, nodes); + toRemove.add(node); + } else { + textNode.text(text); + } + } + } + } + + @Override + public void tail(org.jsoup.nodes.Node node, int depth) { + } + }); + + toRemove.forEach(Node::remove); + + doc.outputSettings().prettyPrint(false); + + String ret = doc.select("body").html(); + ret = StringUtils.trim(ret); + ret = Images.qiniuImgProcessing(ret); + + // cache it + putHTML(markdownText, ret); + + return ret; + }; + + Stopwatchs.start("Md to HTML"); + try { + final Future future = pool.submit(call); + + return future.get(MD_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (final TimeoutException e) { + LOGGER.log(Level.ERROR, "Markdown timeout [md=" + markdownText + "]"); + Callstacks.printCallstack(Level.ERROR, new String[]{"org.b3log"}, null); + + final Set threads = Thread.getAllStackTraces().keySet(); + for (final Thread thread : threads) { + if (thread.getId() == threadId[0]) { + thread.stop(); + + break; + } + } + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Markdown failed [md=" + markdownText + "]", e); + } finally { + pool.shutdownNow(); + + Stopwatchs.end(); + } + + return langPropsService.get("contentRenderFailedLabel"); + } + + private static String toHtmlByLute(final String markdownText) throws Exception { + final URL url = new URL(LUTE_ENGINE_URL); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(1000); + conn.setReadTimeout(7000); + conn.setDoOutput(true); + + try (final OutputStream outputStream = conn.getOutputStream()) { + IOUtils.write(markdownText, outputStream, "UTF-8"); + } + + String ret; + try (final InputStream inputStream = conn.getInputStream()) { + ret = IOUtils.toString(inputStream, "UTF-8"); + } + + conn.disconnect(); + + return ret; + } + + private static String toHtmlByFlexmark(final String markdownText) { + com.vladsch.flexmark.util.ast.Node document = PARSER.parse(markdownText); + + return RENDERER.render(document); + } + + /** + * Gets HTML for the specified markdown text. + * + * @param markdownText the specified markdown text + * @return HTML + */ + private static String getHTML(final String markdownText) { + final String hash = DigestUtils.md5Hex(markdownText); + final JSONObject value = MD_CACHE.get(hash); + if (null == value) { + return null; + } + + return value.optString("data"); + } + + /** + * Puts the specified HTML into cache. + * + * @param markdownText the specified markdown text + * @param html the specified HTML + */ + private static void putHTML(final String markdownText, final String html) { + final String hash = DigestUtils.md5Hex(markdownText); + final JSONObject value = new JSONObject(); + value.put("data", html); + MD_CACHE.put(hash, value); + } + + /** + * Private constructor. + */ + private Markdowns() { + } +} diff --git a/src/main/java/org/b3log/solo/util/Skins.java b/src/main/java/org/b3log/solo/util/Skins.java new file mode 100644 index 00000000..a716f594 --- /dev/null +++ b/src/main/java/org/b3log/solo/util/Skins.java @@ -0,0 +1,296 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateExceptionHandler; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.service.LangPropsService; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.util.Locales; +import org.b3log.latke.util.Stopwatchs; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; + +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +/** + * Skin utilities. + * + * @author Liang Ding + * @version 1.1.6.6, Mar 29, 2019 + * @since 0.3.1 + */ +public final class Skins { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(Skins.class); + + /** + * FreeMarker configuration. + */ + public static final Configuration TEMPLATE_CFG; + + static { + TEMPLATE_CFG = new Configuration(Configuration.VERSION_2_3_28); + TEMPLATE_CFG.setDefaultEncoding("UTF-8"); + final ServletContext servletContext = SoloServletListener.getServletContext(); + TEMPLATE_CFG.setServletContextForTemplateLoading(servletContext, ""); + TEMPLATE_CFG.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + TEMPLATE_CFG.setLogTemplateExceptions(false); + } + + /** + * Properties map. + */ + private static final Map> LANG_MAP = new HashMap<>(); + + /** + * Private constructor. + */ + private Skins() { + } + + /** + * Gets a template with the specified template name. + * + * @param templateName the specified template name + * @return template, returns {@code null} if not found + */ + public static Template getTemplate(final String templateName) { + try { + return Skins.TEMPLATE_CFG.getTemplate(templateName); + } catch (final IOException e) { + LOGGER.log(Level.ERROR, "Gets console template [" + templateName + "] failed", e); + + return null; + } + } + + /** + * Gets a skins template with the specified request and template name. + * + * @param context the specified request context + * @param templateName the specified template name + * @return template, returns {@code null} if not found + */ + public static Template getSkinTemplate(final RequestContext context, final String templateName) { + String templateDirName = (String) context.attr(Keys.TEMAPLTE_DIR_NAME); + if (StringUtils.isBlank(templateDirName)) { + templateDirName = Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME; + } + + try { + return Skins.TEMPLATE_CFG.getTemplate("skins/" + templateDirName + "/" + templateName); + } catch (final IOException e) { + return null; + } + } + + /** + * Fills the specified data model with the current skin's (WebRoot/skins/${skinDirName}/lang/lang_xx_XX.properties) + * and core language (WebRoot/WEB-INF/classes/lang_xx_XX.properties) configurations. + * + * @param localeString the specified locale string + * @param currentSkinDirName the specified current skin directory name + * @param dataModel the specified data model + * @throws ServiceException service exception + */ + public static void fillLangs(final String localeString, String currentSkinDirName, final Map dataModel) + throws ServiceException { + Stopwatchs.start("Fill Skin Langs"); + + try { + // Fills the core language configurations + final BeanManager beanManager = BeanManager.getInstance(); + final LangPropsService langPropsService = beanManager.getReference(LangPropsService.class); + dataModel.putAll(langPropsService.getAll(Latkes.getLocale())); + + if (StringUtils.isBlank(currentSkinDirName)) { + currentSkinDirName = Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME; + } + final String langName = currentSkinDirName + "." + localeString; + Map langs = LANG_MAP.get(langName); + if (null == langs) { + LANG_MAP.clear(); // Collect unused skin languages + + langs = new HashMap<>(); + final String language = Locales.getLanguage(localeString); + final String country = Locales.getCountry(localeString); + final ServletContext servletContext = SoloServletListener.getServletContext(); + final InputStream inputStream = servletContext.getResourceAsStream( + "/skins/" + currentSkinDirName + "/lang/lang_" + language + '_' + country + ".properties"); + if (null != inputStream) { + LOGGER.log(Level.DEBUG, "Loading skin [dirName={0}, locale={1}]", currentSkinDirName, localeString); + final Properties props = new Properties(); + props.load(inputStream); + inputStream.close(); + final Set keys = props.keySet(); + for (final Object key : keys) { + String val = props.getProperty((String) key); + val = replaceVars(val); + langs.put((String) key, val); + } + + LANG_MAP.put(langName, langs); + LOGGER.log(Level.DEBUG, "Loaded skin [dirName={0}, locale={1}, keyCount={2}]", currentSkinDirName, localeString, langs.size()); + } + } + + dataModel.putAll(langs); // Fills the current skin's language configurations + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Fills skin langs failed", e); + + throw new ServiceException(e); + } finally { + Stopwatchs.end(); + } + } + + /** + * Gets all skin directory names. Scans the /skins/ directory, using the subdirectory of it as the skin directory + * name, for example, + *
+     * ${Web root}/skins/
+     *     default/
+     *     mobile/
+     *     classic/
+     * 
. + * + * @return a set of skin name, returns an empty set if not found + */ + public static Set getSkinDirNames() { + final Set ret = new HashSet<>(); + + final ServletContext servletContext = SoloServletListener.getServletContext(); + final Set resourcePaths = servletContext.getResourcePaths("/skins"); + for (final String path : resourcePaths) { + final Path p = Paths.get(path); + final Path file = p.getFileName(); + final String fileName = file.toString(); + if (fileName.startsWith(".") || fileName.endsWith(".md")) { + continue; + } + + ret.add(fileName); + } + + return ret; + } + + /** + * Gets skin directory name from the specified request. + * Refers to 前台皮肤切换 for more details. + * + * @param context the specified request context + * @return directory name, or {@code null} if not found + */ + public static String getSkinDirName(final RequestContext context) { + // 1. Get skin from query + final String specifiedSkin = context.param(Option.CATEGORY_C_SKIN); + if (StringUtils.isNotBlank(specifiedSkin)) { + final Set skinDirNames = Skins.getSkinDirNames(); + if (skinDirNames.contains(specifiedSkin)) { + return specifiedSkin; + } else { + return null; + } + } + + // 2. Get skin from cookie + return getSkinDirNameFromCookie(context.getRequest()); + } + + /** + * Gets skin directory name from the specified request's cookie. + * + * @param request the specified request + * @return directory name, or {@code null} if not found + */ + public static String getSkinDirNameFromCookie(final HttpServletRequest request) { + final Set skinDirNames = Skins.getSkinDirNames(); + boolean isMobile = Solos.isMobile(request); + String skin = null, mobileSkin = null; + final Cookie[] cookies = request.getCookies(); + if (null != cookies) { + for (final Cookie cookie : cookies) { + if (Common.COOKIE_NAME_SKIN.equals(cookie.getName()) && !isMobile) { + final String s = cookie.getValue(); + if (skinDirNames.contains(s)) { + skin = s; + break; + } + } + if (Common.COOKIE_NAME_MOBILE_SKIN.equals(cookie.getName()) && isMobile) { + final String s = cookie.getValue(); + if (skinDirNames.contains(s)) { + mobileSkin = s; + break; + } + } + } + } + + if (StringUtils.isNotBlank(skin)) { + return skin; + } + if (StringUtils.isNotBlank(mobileSkin)) { + return mobileSkin; + } + + return null; + } + + /** + * Replaces all variables of the specified language value. + * + *

+ * Variables: + *

    + *
  • ${servePath}
  • + *
  • ${staticServePath}
  • + *
+ *

+ * + * @param langValue the specified language value + * @return replaced value + */ + private static String replaceVars(final String langValue) { + String ret = StringUtils.replace(langValue, "${servePath}", Latkes.getServePath()); + ret = StringUtils.replace(ret, "${staticServePath}", Latkes.getStaticServePath()); + + return ret; + } +} diff --git a/src/main/java/org/b3log/solo/util/Solos.java b/src/main/java/org/b3log/solo/util/Solos.java new file mode 100644 index 00000000..b18ee950 --- /dev/null +++ b/src/main/java/org/b3log/solo/util/Solos.java @@ -0,0 +1,558 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.logging.Level; +import org.b3log.latke.logging.Logger; +import org.b3log.latke.model.Pagination; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.servlet.RequestContext; +import org.b3log.latke.util.CollectionUtils; +import org.b3log.latke.util.Crypts; +import org.b3log.latke.util.Strings; +import org.b3log.solo.SoloServletListener; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.repository.UserRepository; +import org.json.JSONObject; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * Solo utilities. + * + * @author Liang Ding + * @version 1.9.0.1, May 9, 2019 + * @since 2.8.0 + */ +public final class Solos { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(Solos.class); + + /** + * Favicon API. + */ + public static final String FAVICON_API; + + /** + * Solo User-Agent. + */ + public static final String USER_AGENT = "Solo/" + SoloServletListener.VERSION + "; +https://github.com/b3log/solo"; + + /** + * Cookie expiry in 30 days. + */ + private static final int COOKIE_EXPIRY = 60 * 60 * 24 * 30; + + /** + * Cookie name. + */ + public static final String COOKIE_NAME; + + /** + * Cookie secret. + */ + public static final String COOKIE_SECRET; + + /** + * Cookie HTTP only. + */ + public static final boolean COOKIE_HTTP_ONLY; + + static { + ResourceBundle solo; + try { + solo = ResourceBundle.getBundle("solo"); + } catch (final MissingResourceException e) { + solo = ResourceBundle.getBundle("b3log"); // 2.8.0 向后兼容 + } + + FAVICON_API = solo.getString("faviconAPI"); + } + + static { + String cookieNameConf = Latkes.getLatkeProperty("cookieName"); + if (StringUtils.isBlank(cookieNameConf)) { + cookieNameConf = "solo"; + } + COOKIE_NAME = cookieNameConf; + + String cookieSecret = Latkes.getLatkeProperty("cookieSecret"); + if (StringUtils.isBlank(cookieSecret)) { + cookieSecret = RandomStringUtils.randomAlphanumeric(8); + } + COOKIE_SECRET = cookieSecret; + + COOKIE_HTTP_ONLY = Boolean.valueOf(Latkes.getLocalProperty("cookieHttpOnly")); + } + + /** + * Constructs a successful result. + * + * @return result + */ + public static JSONObject newSucc() { + return new JSONObject().put(Keys.CODE, 0).put(Keys.MSG, ""); + } + + /** + * Constructs a failed result. + * + * @return result + */ + public static JSONObject newFail() { + return new JSONObject().put(Keys.CODE, -1).put(Keys.MSG, "System is abnormal, please try again later"); + } + + private static long uploadTokenCheckTime; + private static long uploadTokenTime; + private static String uploadToken = ""; + private static String uploadURL = "https://hacpai.com/upload/client"; + private static String uploadMsg = ""; + + /** + * Gets upload token. + * + * @param context the specified context + * @return upload token and URL, returns {@code null} if not found + */ + public static JSONObject getUploadToken(final RequestContext context) { + try { + final JSONObject currentUser = getCurrentUser(context.getRequest(), context.getResponse()); + if (null == currentUser) { + return null; + } + + final String userName = currentUser.optString(User.USER_NAME); + final String userB3Key = currentUser.optString(UserExt.USER_B3_KEY); + if (StringUtils.isBlank(userB3Key)) { + return null; + } + + final long now = System.currentTimeMillis(); + if (3600000 >= now - uploadTokenTime) { + return new JSONObject(). + put(Common.UPLOAD_TOKEN, uploadToken). + put(Common.UPLOAD_URL, uploadURL). + put(Common.UPLOAD_MSG, uploadMsg); + } + + if (15000 >= now - uploadTokenCheckTime) { + return new JSONObject(). + put(Common.UPLOAD_TOKEN, uploadToken). + put(Common.UPLOAD_URL, uploadURL). + put(Common.UPLOAD_MSG, uploadMsg); + } + + final JSONObject requestJSON = new JSONObject().put(User.USER_NAME, userName).put(UserExt.USER_B3_KEY, userB3Key); + final HttpResponse res = HttpRequest.post("https://hacpai.com/apis/upload/token").trustAllCerts(true). + body(requestJSON.toString()).connectionTimeout(3000).timeout(7000).header("User-Agent", Solos.USER_AGENT).send(); + uploadTokenCheckTime = now; + if (HttpServletResponse.SC_OK != res.statusCode()) { + return null; + } + res.charset("UTF-8"); + final JSONObject result = new JSONObject(res.bodyText()); + if (0 != result.optInt(Keys.CODE)) { + uploadMsg = result.optString(Keys.MSG); + LOGGER.log(Level.ERROR, uploadMsg); + + return null; + } + + final JSONObject data = result.optJSONObject(Common.DATA); + uploadTokenTime = now; + uploadToken = data.optString("uploadToken"); + uploadURL = data.optString("uploadURL"); + uploadMsg = ""; + + return new JSONObject(). + put(Common.UPLOAD_TOKEN, uploadToken). + put(Common.UPLOAD_URL, uploadURL). + put(Common.UPLOAD_MSG, uploadMsg); + } catch (final Exception e) { + LOGGER.log(Level.ERROR, "Gets upload token failed", e); + + return null; + } + } + + /** + * Sanitizes the specified file name. + * + * @param unsanitized the specified file name + * @return sanitized file name + */ + public static String sanitizeFilename(final String unsanitized) { + return unsanitized. + replaceAll("[^(a-zA-Z0-9\\u4e00-\\u9fa5\\.)]", ""). + replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~]", ""). + replaceAll("\\s", ""); + } + + /** + * Adds noindex header for Google. https://github.com/b3log/solo/issues/12631 + *

+ * 使用“noindex”阻止搜索引擎将您的网页编入索引 https://support.google.com/webmasters/answer/93710?hl=zh-Hans + *

+ * + * @param context the specified context + */ + public static void addGoogleNoIndex(final RequestContext context) { + context.setHeader("X-Robots-Tag", "noindex"); + } + + /** + * Gets the current logged-in user. + * + * @param request the specified request + * @param response the specified response + * @return the current logged-in user, returns {@code null} if not found + */ + public static JSONObject getCurrentUser(final HttpServletRequest request, final HttpServletResponse response) { + final Cookie[] cookies = request.getCookies(); + if (null == cookies || 0 == cookies.length) { + return null; + } + + final BeanManager beanManager = BeanManager.getInstance(); + final UserRepository userRepository = beanManager.getReference(UserRepository.class); + try { + for (int i = 0; i < cookies.length; i++) { + final Cookie cookie = cookies[i]; + if (!COOKIE_NAME.equals(cookie.getName())) { + continue; + } + + final String value = Crypts.decryptByAES(cookie.getValue(), COOKIE_SECRET); + final JSONObject cookieJSONObject = new JSONObject(value); + + final String userId = cookieJSONObject.optString(Keys.OBJECT_ID); + if (StringUtils.isBlank(userId)) { + break; + } + + JSONObject user = userRepository.get(userId); + if (null == user) { + break; + } + + final String b3Key = user.optString(UserExt.USER_B3_KEY); + final String tokenVal = cookieJSONObject.optString(Keys.TOKEN); + final String token = StringUtils.substringBeforeLast(tokenVal, ":"); + if (StringUtils.equals(b3Key, token)) { + login(user, response); + + return user; + } + } + } catch (final Exception e) { + LOGGER.log(Level.TRACE, "Parses cookie failed, clears the cookie [name=" + COOKIE_NAME + "]"); + + final Cookie cookie = new Cookie(COOKIE_NAME, null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + + return null; + } + + /** + * Logins the specified user from the specified request. + * + * @param response the specified response + * @param user the specified user + */ + public static void login(final JSONObject user, final HttpServletResponse response) { + try { + final String userId = user.optString(Keys.OBJECT_ID); + final JSONObject cookieJSONObject = new JSONObject(); + cookieJSONObject.put(Keys.OBJECT_ID, userId); + final String b3Key = user.optString(UserExt.USER_B3_KEY); + final String random = RandomStringUtils.randomAlphanumeric(8); + cookieJSONObject.put(Keys.TOKEN, b3Key + ":" + random); + final String cookieValue = Crypts.encryptByAES(cookieJSONObject.toString(), COOKIE_SECRET); + final Cookie cookie = new Cookie(COOKIE_NAME, cookieValue); + cookie.setPath("/"); + cookie.setMaxAge(COOKIE_EXPIRY); + cookie.setHttpOnly(COOKIE_HTTP_ONLY); + response.addCookie(cookie); + } catch (final Exception e) { + LOGGER.log(Level.WARN, "Can not write cookie", e); + } + } + + /** + * Logouts the specified user. + * + * @param request the specified request + * @param response the specified response + * @return {@code true} if succeed, otherwise returns {@code false} + */ + public static void logout(final HttpServletRequest request, final HttpServletResponse response) { + if (null != response) { + final Cookie cookie = new Cookie(COOKIE_NAME, null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + } + + /** + * Checks whether the current request is made by a logged in user + * (including default user and administrator lists in users). + * + * @param context the specified request context + * @return {@code true} if the current request is made by logged in user, returns {@code false} otherwise + */ + public static boolean isLoggedIn(final RequestContext context) { + return null != Solos.getCurrentUser(context.getRequest(), context.getResponse()); + } + + /** + * Checks whether the current request is made by logged in administrator. + * + * @param context the specified request context + * @return {@code true} if the current request is made by logged in + * administrator, returns {@code false} otherwise + */ + public static boolean isAdminLoggedIn(final RequestContext context) { + final JSONObject user = getCurrentUser(context.getRequest(), context.getResponse()); + if (null == user) { + return false; + } + + return Role.ADMIN_ROLE.equals(user.optString(User.USER_ROLE)); + } + + /** + * Checks whether need password to view the specified article with the specified request. + *

+ * Checks session, if not represents, checks article property {@link Article#ARTICLE_VIEW_PWD view password}. + *

+ *

+ * The blogger itself dose not need view password never. + *

+ * + * @param context the specified request context + * @param article the specified article + * @return {@code true} if need, returns {@code false} otherwise + */ + public static boolean needViewPwd(final RequestContext context, final JSONObject article) { + final String articleViewPwd = article.optString(Article.ARTICLE_VIEW_PWD); + + if (StringUtils.isBlank(articleViewPwd)) { + return false; + } + + final HttpServletRequest request = context.getRequest(); + if (null == request) { + return true; + } + + final HttpSession session = request.getSession(); + if (null != session) { + Map viewPwds = (Map) session.getAttribute(Common.ARTICLES_VIEW_PWD); + if (null == viewPwds) { + viewPwds = new HashMap<>(); + } + + if (articleViewPwd.equals(viewPwds.get(article.optString(Keys.OBJECT_ID)))) { + return false; + } + } + + final HttpServletResponse response = context.getResponse(); + final JSONObject currentUser = getCurrentUser(request, response); + + return !(null != currentUser && !Role.VISITOR_ROLE.equals(currentUser.optString(User.USER_ROLE))); + } + + /** + * Checks the specified request is made from a mobile device. + * + * @param request the specified request + * @return {@code true} if it is, returns {@code false} otherwise + */ + public static boolean isMobile(final HttpServletRequest request) { + final Object val = request.getAttribute(Keys.HttpRequest.IS_MOBILE_BOT); + if (!(val instanceof Boolean)) { + return false; + } + + return (boolean) val; + } + + /** + * Checks the specified request is made from a bot. + * + * @param request the specified request + * @return {@code true} if it is, returns {@code false} otherwise + */ + public static boolean isBot(final HttpServletRequest request) { + final Object val = request.getAttribute(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT); + if (!(val instanceof Boolean)) { + return false; + } + + return (boolean) val; + } + + /** + * Gets the default user avatar URL.. + * + * @return default user avatar URL + */ + public static String getDefaultAvatar() { + return Latkes.getStaticServePath() + "/images/default-user-thumbnail.png"; + } + + /** + * Clones a JSON object from the specified source object. + * + * @param src the specified source object + * @return cloned object + */ + public static JSONObject clone(final JSONObject src) { + return new JSONObject(src, CollectionUtils.jsonArrayToArray(src.names(), String[].class)); + } + + /** + * Builds pagination request with the specified path. + * + * @param path the specified path, "/{page}/{pageSize}/{windowSize}" + * @return pagination request json object, for example, + *
+     * {
+     *     "paginationCurrentPageNum": int,
+     *     "paginationPageSize": int,
+     *     "paginationWindowSize": int
+     * }
+     * 
+ */ + public static JSONObject buildPaginationRequest(final String path) { + final Integer currentPageNum = getCurrentPageNum(path); + final Integer pageSize = getPageSize(path); + final Integer windowSize = getWindowSize(path); + + final JSONObject ret = new JSONObject(); + ret.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum); + ret.put(Pagination.PAGINATION_PAGE_SIZE, pageSize); + ret.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize); + + return ret; + } + + /** + * Default page size. + */ + private static final int DEFAULT_PAGE_SIZE = 15; + + /** + * Default window size. + */ + private static final int DEFAULT_WINDOW_SIZE = 20; + + /** + * Gets the request page number from the specified path. + * + * @param path the specified path + * @return page number, returns {@code 1} if the specified request URI can not convert to an number + */ + private static int getCurrentPageNum(final String path) { + if (StringUtils.isBlank(path) || path.equals("/")) { + return 1; + } + final String currentPageNumber = path.split("/")[0]; + if (!Strings.isNumeric(currentPageNumber)) { + return 1; + } + + return Integer.valueOf(currentPageNumber); + } + + /** + * Gets the request page size from the specified path. + * + * @param path the specified path + * @return page number, returns {@value #DEFAULT_PAGE_SIZE} if the specified request URI can not convert to an number + */ + private static int getPageSize(final String path) { + if (StringUtils.isBlank(path)) { + return DEFAULT_PAGE_SIZE; + } + final String[] parts = path.split("/"); + if (1 >= parts.length) { + return DEFAULT_PAGE_SIZE; + } + final String pageSize = parts[1]; + if (!Strings.isNumeric(pageSize)) { + return DEFAULT_PAGE_SIZE; + } + + return Integer.valueOf(pageSize); + } + + /** + * Gets the request window size from the specified path. + * + * @param path the specified path + * @return page number, returns {@value #DEFAULT_WINDOW_SIZE} if the specified request URI can not convert to an number + */ + private static int getWindowSize(final String path) { + if (StringUtils.isBlank(path)) { + return DEFAULT_WINDOW_SIZE; + } + final String[] parts = path.split("/"); + if (2 >= parts.length) { + return DEFAULT_WINDOW_SIZE; + } + final String windowSize = parts[2]; + if (!Strings.isNumeric(windowSize)) { + return DEFAULT_WINDOW_SIZE; + } + + return Integer.valueOf(windowSize); + } + + /** + * Private constructor. + */ + private Solos() { + } +} diff --git a/src/main/java/org/b3log/solo/util/package-info.java b/src/main/java/org/b3log/solo/util/package-info.java new file mode 100644 index 00000000..45a317da --- /dev/null +++ b/src/main/java/org/b3log/solo/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Utilities. + */ +package org.b3log.solo.util; diff --git a/src/main/resources/docker/latke.properties b/src/main/resources/docker/latke.properties new file mode 100644 index 00000000..8221aaca --- /dev/null +++ b/src/main/resources/docker/latke.properties @@ -0,0 +1,33 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Latke configurations for docker. +# Version: 2.0.0.0, Mar 19, 2019 +# Author: Liang Ding +# + +#### Server #### +# Browser visit protocol +serverScheme=http +# Browser visit domain name +serverHost=localhost + +#### Runtime Mode #### +### runtimeMode=DEVELOPMENT +runtimeMode=PRODUCTION diff --git a/src/main/resources/etc/header.txt b/src/main/resources/etc/header.txt new file mode 100644 index 00000000..b37a02db --- /dev/null +++ b/src/main/resources/etc/header.txt @@ -0,0 +1,15 @@ +Solo - A small and beautiful blogging system written in Java. +Copyright (c) ${year}, ${devTeam} + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/src/main/resources/lang_en_US.properties b/src/main/resources/lang_en_US.properties new file mode 100644 index 00000000..45325a8b --- /dev/null +++ b/src/main/resources/lang_en_US.properties @@ -0,0 +1,388 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Solo language configurations(en_US). +# Version: 2.34.0.0, Aug 18, 2019 +# Author: Liang Ding +# Author: Liyuan Li +# Author: Dongxu Wang +# + +changeUserNameTipLabel=(Please keep the blog, community and GitHub user names in the same, otherwise it will not be able to login) +setMobileLabel=Set to mobile skin +configSiteLabel=Go to the site link to configure GitHub, Twitter, etc. +hljsThemeLabel=hljs theme: +tocLabel=ToC +removeUnusedArchivesLabel=Remove Unused Archives +syncGitHubLabel=Allow sync GitHub repo: +pullGitHubLabel=Allow pull GitHub repo: +queryUserFailedLabel=Query user info failed +commentContentLabel=Content +cancelLabel=Cancel +langLabel=en_US +pushSuccLabel=Sync Successful +pushToHacpaiLabel=Sync to Hacpai +getUploadTokenErrLabel=Get community file storage service upload token failed +startToUseLabel=Start +clearDataLabel=Clear data +exportDataLabel=Export data +syncToCommunityLabel=Sync to community +cntLabel= +reply1Label=Reply +nextArticleLabel=Next Article +onlineVisitorLabel=Online +cntMonthLabel=Month +cntArticleLabel=Article +useGitHubAccountLoginLabel=Get started after logging GitHub account +chageLabel=Change +useTumbnailLabel=Use Tumbnail +iconLabel=Icon +icon1Label=Icon: +contentRenderFailedLabel=Content render failed, please report this problem to help us enhance it, thank you ♥ +userNameInvalidLabel=Username only allow alphabet or number! +sponsorLabel=Become a Sponsor +addBoldLabel=Add bold text +addItalicLabel=Add italic text +insertQuoteLabel=Insert a quote +addBulletedLabel=Add a bulleted list +addNumberedListLabel=Add a numbered list +undoLabel=Undo +redoLabel=Redo +fullscreenLabel=Fullscreen +helpLabel=Help +previewLabel=Preview +uploadFilesLabel=Upload File +howConfigLabel=How? +duplicatedCategoryURILabel=Duplicated category URI! +categoryURITooLongLabel=Category URI is too long to save! +duplicatedCategoryLabel=Duplicated category! +addArticleWithTagFirstLabel=Please add an article with tag [{tag}] +searchLabel=Search +dynamicLabel=Dynamic +exportSQLLabel=Export SQL file +exportJSONLabel=Export JSON file +exportHexoLabel=Export Hexo file +footerContent1Label=Footer: +userAvatar1Label=Avatar: +staticErrorLabel=

Latke Configuraton Error

\ +



Please visit Latke \u914D\u7F6E\u5256\u6790 to solve it.


+markdownHelpLabel=
Headings
# First-level heading
#### Fourth-level heading
\ +
Link
[link text here](link.address.here)
\ +
Emphasized text
*italics* or _italics_
**boldface** or __boldface__
***boldface+italics*** or ___boldface+italics___
\ +
List
* An item in an enumerated (unordered)
1. An item in an enumerated (ordered)
\ +
Code
`var markDown = undefined`
\ +
Image
![alt text](/path/to/img.jpg 'Title')
\ +
All Grammar
+editType1Label=Editor Type: +typeLabel=Type +onlineVisitor1Label=Online Visitors: +noDataLabel=Lazy guy, has nothing! +linkDescriptionLabel=Link Description +linkDescription1Label=Description: +descriptionLabel=Description +addressInvalidLabel=start protocol, e.g.: http:// +ToolLabel=Tool +titleAndContentLabel=Title+Content +titleAndAbstractLabel=Title+Abstract +titleOnlyLabel=Title only +articleListDisplay1Label=Article List Style: +checkingVersionLabel=Checking for updates.... +upToDateLabel=Your Solo is up to date \u2728 +outOfDateLabel=Your Solo is out of date \uD83D\uDE22, download the latest version: +aboutLabel=About +aboutContentLabel=

Solo \ +is a small and beautiful Java blogging system developed and maintained by the B3log open source community.

\ +

We are building an independent blog + community ecology. Sound interesting? Join us!

+confirmLabel=Confirm +adminConsoleLabel=Admin +adminIndexLabel=Admin Index +postArticleLabel=Post +articleListLabel=Articles +commentListLabel=Comments +draftListLabel=Drafts +userManageLabel=Users +commonUserLabel=Common User +visitorUserLabel=visitor +updateUserLabel=Update User +updateCategoryLabel=Update Category +linkManagementLabel=Links +categoryListLabel=Category +pluginMgmtLabel=Plugins +pluginNameLabel=Name +versionLabel=Version +statusLabel=Status +enabledLabel=Enabled +disabledLabel=Disabled +enableLabel=Enable +disableLabel=Disable +settingLabel=setting +preferenceLabel=Preference +localeString1Label=Language: +timeZoneId1Label=Time Zone: +adminLabel=Admin +roleLabel=Role +administratorLabel=Administrator +logoutLabel=Logout +initLabel=Initial +popTagsLabel=Popular Tags +tag1Label=Tag: +tags1Label=Tags: +tags1WithTips1Label=Tags(splits with comma): +recentArticlesLabel=Recent Articles +recentCommentsLabel=Recent Comments +postCommentsLabel=Post Comment +mostCommentArticlesLabel=Most Comment Articles +mostViewCountArticlesLabel=Most View Articles +em00Label=:smile: +em01Label=:joy: +em02Label=:stuck_out_tongue_winking_eye: +em03Label=:persevere: +em04Label=:sob: +em05Label=:cold_sweat: +em06Label=:rage: +em07Label=:triumph: +em08Label=:eyes: +em09Label=:scream: +em10Label=:sunglasses: +em11Label=:yum: +em12Label=:heart: +em13Label=:broken_heart: +em14Label=:smiling_imp: +linkLabel=Friend Links +sumLabel= +pageLabel=Page +commentLabel=Comment +linkTitleLabel=Link Title +linkTitle1Label=Title: +updateLabel=Update +removeLabel=Remove +putTopLabel=Put Top +cancelPutTopLabel=Cancel Put Top +downloadCountLabel=Count +sizeLabel=Size +uploadDateLabel=Upload Date +downloadURLLabel=Download URL +downloadLabel=Download +createDateLabel=Create Date +updateDateLabel=Update Date +dateLabel=Date +titleLabel=Title +title1Label=Title: +content1Label=Content: +abstract1Label=Summary: +publishLabel=Publish +unPublishLabel=Un Publish +urlLabel=URL +url1Label=URL: +addLinkLabel=Add Link +updateLinkLabel=Update Link +archiveLabel=Archive +archive1Label=archive: +yearLabel= +monthLabel= +navMgmtLabel=Navigation +navLabel=Navigation +openMethod1Label=Target: +openMethodLabel=Target +linkEmptyLabel=Link is empty +targetSelfLabel=Open in this window / frame +targetBlankLabel=Open in new window (_blank) +targetParentLabel=Open in parent window / frame (_parent) +targetTopLabel=Open in top frame (replaces all frames) (_top) +othersLabel=Others +fileListLabel=Files +submitUploadLabel=Upload +fileNameLabel=File Name +paramSettingsLabel=Parameters +configSettingsLabel=Configuration +skinLabel=Theme +signLabel=Signs +sign1Label=Signs: +noSignLabel=None +signIsNullLabel=This Sign is Null +statisticLabel=Blog Statistic +viewLabel=View +countLabel=Posts +viewCount1Label=View Count: +articleCount1Label=Article Count: +commentCountLabel=Comment Count +commentCount1Label=Comment Count: +getDateLabel=Get Date +selectDateLabel=Select Date +selectDate1Label=Select Date: +selectLabel=Select +importLabel=Import +userNameLabel=Username +userName1Label=Username: +userLabel=User +userPassword1Label=Password: +userPasswordLabel=Password +userURL1Label=Link: +userURLLabel=Link +categoryLabel=Category +noticeBoard1Label=Notice Board: +noticeBoardLabel=Notice Board +htmlhead1Label=HTML head: +indexTagDisplayCntLabel=Index Tag Display Count +indexTagDisplayCnt1Label=Index Tag Display Count: +indexRecentArticleDisplayCntLabel=Recent Article Display Count +indexRecentArticleDisplayCnt1Label=Recent Article Display Count: +indexRecentCommentDisplayCntLabel=Recent Comment Display Count +indexRecentCommentDisplayCnt1Label=Recent Comment Display Count: +indexMostCommentArticleDisplayCntLabel=Most Comment Article Display Count +indexMostCommentArticleDisplayCnt1Label=Most Comment Article Count: +indexMostViewArticleDisplayCntLabel=Most View Article Display Count +indexMostViewArticleDisplayCnt1Label=Most View Article Display Count: +relevantArticlesDisplayCntLabel=Relevant Article Display Count +relevantArticlesDisplayCnt1Label=Relevant Article Display Count: +randomArticlesDisplayCntLabel=Random Article Display Count +randomArticlesDisplayCnt1Label=Random Article Display Count: +externalRelevantArticlesDisplayCntLabel=External Relevant Article Display Count +externalRelevantArticlesDisplayCnt1Label=External Relevant Article Count: +windowSizeLabel=Pagination Window Size +windowSize1Label=Pagination Window Size: +pageSizeLabel=Pagination Page Size +pageSize1Label=Pagination Page Size: +blogTitle1Label=Blog Title: +blogSubtitle1Label=Blog Subtitle: +blogHost1Label=Blog Domain: +submmitCommentLabel=Commit Comment +saveLabel=Save +tagLabel=Tag +tagsLabel=Tags +indexLabel=Index +nextArticle1Label=Next: +previousArticle1Label=Previous: +updatedLabel=Updated! +topArticleLabel=Top! +previousPageLabel=Previous Page +nextPagePabel=Next Page +firstPageLabel=First Page +lastPageLabel=Last Page +returnTo1Label=Return to: +previousStepLabel=Previous +nextStepLabel=Next +atomLabel=Atom +relevantArticlesLabel=Relevant Articles +relevantArticles1Label=Relevant Articles: +randomArticlesLabel=Random Articles +randomArticles1Label=Random Articles: +externalRelevantArticlesLabel=External Relevant Articles +externalRelevantArticles1Label=External Relevant Articles: +metaKeywords1Label=Meta Keywords: +metaDescription1Label=Meta Description: +removeUnusedTagsLabel=Remove Unused Tags +goTopLabel=Top +permalink1Label=Permalink: +permalinkLabel=Permalink +welcomeToSoloLabel=Welcome to +killBrowserLabel=

Let's kill outdated and insecure browser!

Let's kill outdated and insecure browser for browser evolution, human progress and better experience.

You can download

Tip: Remove "Util.killIE();" in /js/common.js can support all browser.
+closeLabel=Close +closeForeverLabel=Close Forever +readmoreLabel=Read more\u00BB +readmore2Label=Read more +replyLabel=Reply\u00BB +homeLabel=Home +enableArticleUpdateHint1Label=Enable Article Update Hint: +allowVisitDraftViaPermalink1Label=Allow Visit Draft Via Link: +allowComment1Label=Allow Comment: +allowCommentLabel=Allow Comment +feedOutputModel1Label=Feed Output Mode: +feedOutputCntLabel=Feed Output Count +feedOutputCnt1Label=Feed Output Count: +customVars1Label=Custom template vars: +abstractLabel=Abstract +fullContentLabel=Full Content +author1Label=Author: +authorLabel=Author +keyOfSolo1Label=B3log Key: +articleLabel=Article +tagArticlesLabel=Tag Articles +dateArticlesLabel=Archive Date Articles +authorArticlesLabel=Author Articles +indexArticleLabel=Index Articles +userTemplatePageLabel=User Template Page +allTagsLabel=Tag Cloud +customizedPageLabel=Customized Page +killBrowserPageLabel=Kill Browser Page +pageNumLabel=Page Number +articleViewPwdLabel=View Password +articleViewPwd1Label=View Password: +#### +forbiddenLabel=Forbidden Access! +notFoundLabel=Not Found! +unPulbishSuccLabel=Un Publish Successfully +unPulbishFailLabel=Un Publish Fail +removeSuccLabel=Remove Successfully +removeFailLabel=Remove Fail +putTopSuccLabel=Put Top Successfully +putTopFailLabel=Put Top Fail +cancelTopSuccLabel=Cancel Top Successfully +cancelTopFailLabel=Cancel Top Fail +addSuccLabel=Add Successfully +addFailLabel=Add Fail +updateSuccLabel=Update Successfully +updateFailLabel=Update Fail +setFailLabel=Set Fail +setSuccLabel=Set Successfully +getFailLabel=Get Fail +getSuccLabel=Get Successfully +noCommentLabel=No Comment +inputErrorLabel=Input Error! +gotoLabel=Go +passwordNotMatchLabel=Password Not Match +notAllowCommentLabel=Not allow comment! +nameTooLongLabel=Sorry, your username must be between 2 and 20 characters long. +categoryTooLongLabel=Sorry, category name must be between 2 and 32 characters long. +urlInvalidLabel=URL is invalid +nonNegativeIntegerOnlyLabel=Non-Negative integer only +commentContentCannotEmptyLabel=Sorry, your content must be between 2 and 500 characters long. +loadingLabel=Loading.... +titleEmptyLabel=Title is empty +contentEmptyLabel=Content is empty +orderEmptyLabel=Order is empty +abstractEmptyLabel=Abstract is empty +tagsEmptyLabel=Tags is empty (do not include specific symbol) +addressEmptyLabel=Address is empty +duplicatedPermalinkLabel=Duplicated permalink! +invalidPermalinkFormatLabel=Invalid permalink format! +duplicatedUserNameLabel=Duplicated username! +refreshAndRetryLabel=Please refresh and try again! +editorLeaveLabel=Content is not null, Do you leave? +editorPostLabel=Content is not null, Do you clear? +changeRoleLabel=Change Role +#### +confirmRemoveLabel=Are You Remove +confirmInitLabel=Are You Sure? +###### Common ###### +mobileLabel=Mobile Theme +helloWorld.title=\u4E16\u754C\uFF0C\u4F60\u597D\uFF01 +helloWorld.content=Solo \u535A\u5BA2\u7CFB\u7EDF\u5DF2\u7ECF\u521D\u59CB\u5316\u5B8C\u6BD5\uFF0C\u53EF\u5728\u7BA1\u7406\u540E\u53F0 - \u5DE5\u5177 - \u504F\u597D\u8BBE\u5B9A\u4E2D\u8C03\u6574\u66F4\u591A\u7EC6\u8282\u8BBE\u7F6E\u3002\u5982\u679C\u9700\u8981\u5BFC\u5165\u5DF2\u6709\u535A\u5BA2\u6587\u7AE0\uFF0C\u8BF7\u53C2\u8003\u6587\u6863 [Hexo/Jekyll/Markdown \u6587\u4EF6\u5BFC\u5165](https://hacpai.com/article/1498490209748)\u3002\n\n\ +\u5BF9\u4E86\uFF0C\u51FA\u4E8E\u5B89\u5168\u8003\u8651\u8BF7\u5C3D\u5FEB\u5B8C\u6210\u5982\u4E0B\u64CD\u4F5C\uFF1A\n\n\ +1. \u4F7F\u7528 GitHub \u8D26\u53F7\u767B\u5F55[\u793E\u533A](https://hacpai.com)\n\ +2. \u5728\u793E\u533A[\u4E2A\u4EBA\u8BBE\u7F6E - B3](https://hacpai.com/settings/b3) \u4E2D\u66F4\u65B0 B3 Key\n\ +3. \u5728 Solo \u7BA1\u7406\u540E\u53F0 - \u5DE5\u5177 - \u7528\u6237\u7BA1\u7406\u4E2D\u4E5F\u8FDB\u884C\u540C\u6837\u7684 B3 Key \u66F4\u65B0\n\n\ +\u53E6\u5916\uFF0CSolo \u4F1A\u6BCF\u5929\u81EA\u52A8\u5BFC\u51FA\u4F60\u7684\u535A\u5BA2\u6587\u7AE0\u5230 GitHub \u4ED3\u5E93\uFF08[\u793A\u4F8B](https://github.com/88250/solo-blog)\uFF09\uFF1A\n\n\ +1. \u6709\u673A\u4F1A\u8BA9\u66F4\u591A\u4EBA\u770B\u5230\u4F60\u7684\u6587\u7AE0\n\ +2. \u81EA\u52A8\u5907\u4EFD\u6587\u7AE0\u6570\u636E\n\n\ +\u8BE5\u529F\u80FD\u9ED8\u8BA4\u662F\u5F00\u542F\u7684\uFF0C\u5982\u679C\u4E0D\u9700\u8981\u8BF7\u5230\u7BA1\u7406\u540E\u53F0 - \u5DE5\u5177 - \u504F\u597D\u8BBE\u5B9A - \u53C2\u6570\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002\u5F00\u542F\u65F6\u4F1A\u81EA\u52A8\u521B\u5EFA/\u66F4\u65B0 `solo-blog` \u4ED3\u5E93\uFF0C\u8BF7\u6CE8\u610F\u4F60\u6CA1\u6709\u8BE5\u540C\u540D\u4ED3\u5E93\u4EE5\u514D\u6570\u636E\u88AB\u8986\u76D6\u3002\n\n\ +\u6700\u540E\uFF0C\u5982\u679C\u4F60\u89C9\u5F97 Solo \u5F88\u8D5E\uFF0C\u8BF7\u5230[\u9879\u76EE\u4E3B\u9875](https://github.com/b3log/solo)\u7ED9\u9897\u661F\u9F13\u52B1\u4E00\u4E0B :heart: +helloWorld.comment.content=\u5199\u535A\u5BA2\u9700\u8981\u575A\u6301\uFF0C\u76F8\u4FE1\u79EF\u7D2F\u540E\u5FC5\u7136\u4F1A\u6709\u6536\u83B7\uFF0C\u6211\u4EEC\u4E00\u8D77\u52AA\u529B\u52A0\u6CB9 :smile: +articleContentPwd=This article need password to view. +ok=Ok diff --git a/src/main/resources/lang_zh_CN.properties b/src/main/resources/lang_zh_CN.properties new file mode 100644 index 00000000..506edc70 --- /dev/null +++ b/src/main/resources/lang_zh_CN.properties @@ -0,0 +1,388 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Solo default language configurations(zh_CN). +# Version: 2.34.0.0, Aug 18, 2019 +# Author: Liang Ding +# Author: Liyuan Li +# Author: Dongxu Wang +# + +changeUserNameTipLabel=\uFF08\u8BF7\u4FDD\u6301\u535A\u5BA2\u7AEF\u3001\u793E\u533A\u4EE5\u53CA GitHub \u4E09\u4E2A\u5730\u65B9\u7528\u6237\u540D\u4E00\u81F4\uFF0C\u5426\u5219\u4F1A\u5BFC\u81F4\u65E0\u6CD5\u767B\u5F55\uFF09 +setMobileLabel=\u8BBE\u7F6E\u4E3A\u79FB\u52A8\u7AEF\u76AE\u80A4 +configSiteLabel=\u524D\u5F80\u914D\u7F6E GitHub\uFF0CTwitter \u7B49\u7AD9\u70B9\u94FE\u63A5 +hljsThemeLabel=\u4EE3\u7801\u9AD8\u4EAE\u4E3B\u9898\uFF1A +tocLabel=\u76EE\u5F55 +removeUnusedArchivesLabel=\u79FB\u9664\u672A\u4F7F\u7528\u5B58\u6863 +syncGitHubLabel=\u5141\u8BB8\u540C\u6B65 GitHub \u4ED3\u5E93\uFF1A +pullGitHubLabel=\u5141\u8BB8\u62C9\u53D6 GitHub \u4ED3\u5E93\uFF1A +queryUserFailedLabel=\u67E5\u8BE2\u7528\u6237\u4FE1\u606F\u5931\u8D25 +commentContentLabel=\u8BC4\u8BBA\u5185\u5BB9 +cancelLabel=\u53D6\u6D88 +langLabel=zh_CN +pushSuccLabel=\u63A8\u9001\u6210\u529F +pushToHacpaiLabel=\u63A8\u9001\u5230\u793E\u533A +getUploadTokenErrLabel=\u83B7\u53D6\u793E\u533A\u6587\u4EF6\u5B58\u50A8\u670D\u52A1\u4E0A\u4F20\u51ED\u8BC1\u5F02\u5E38 +startToUseLabel=\u5F00\u59CB\u4F7F\u7528 +clearDataLabel=\u6570\u636E\u6E05\u7406 +exportDataLabel=\u6570\u636E\u5BFC\u51FA +syncToCommunityLabel=\u540C\u6B65\u5230\u793E\u533A +cntLabel=\u4E2A +reply1Label=\u56DE\u590D +nextArticleLabel=\u4E0B\u4E00\u7BC7 +onlineVisitorLabel=\u5F53\u524D\u8BBF\u5BA2 +cntMonthLabel=\u4E2A\u6708 +cntArticleLabel=\u7BC7\u6587\u7AE0 +useGitHubAccountLoginLabel=\u767B\u5F55 GitHub \u8D26\u53F7\u540E\u5373\u53EF\u5F00\u59CB\u4F7F\u7528 +chageLabel=\u66F4\u6362 +useTumbnailLabel=\u542F\u7528\u914D\u56FE +iconLabel=\u56FE\u6807 +icon1Label=\u56FE\u6807\uFF1A +contentRenderFailedLabel=\u5185\u5BB9\u6E32\u67D3\u51FA\u73B0\u4E86\u4E00\u4E9B\u95EE\u9898\uFF0C\u8BF7\u5230\u8FD9\u91CC\u53CD\u9988\u95EE\u9898\u4EE5\u5E2E\u52A9\u6211\u4EEC\u8FDB\u884C\u6539\u8FDB\uFF0C\u975E\u5E38\u611F\u8C22 ♥ +userNameInvalidLabel=\u7528\u6237\u540D\u53EA\u80FD\u662F\u5B57\u6BCD\u6216\u6570\u5B57\uFF01 +sponsorLabel=\u6210\u4E3A\u8D5E\u52A9\u8005 +addBoldLabel=\u6DFB\u52A0\u7C97\u4F53 +addItalicLabel=\u6DFB\u52A0\u659C\u4F53 +insertQuoteLabel=\u63D2\u5165\u5F15\u7528 +addBulletedLabel=\u6DFB\u52A0\u65E0\u5E8F\u5217\u8868 +addNumberedListLabel=\u6DFB\u52A0\u6709\u5E8F\u5217\u8868 +undoLabel=\u64A4\u9500 +redoLabel=\u6062\u590D +helpLabel=\u5E2E\u52A9 +fullscreenLabel=\u5168\u5C4F +previewLabel=\u9884\u89C8 +uploadFilesLabel=\u4E0A\u4F20\u6587\u4EF6 +howConfigLabel=\u5982\u4F55\u914D\u7F6E\uFF1F +duplicatedCategoryURILabel=\u5206\u7C7B URI \u91CD\u590D\uFF01 +categoryURITooLongLabel=\u5206\u7C7B URI \u592A\u957F\uFF0C\u5E76\u5C3D\u91CF\u4F7F\u7528\u82F1\u6587\u5B57\u7B26\uFF01 +duplicatedCategoryLabel=\u5206\u7C7B\u91CD\u590D\uFF01 +addArticleWithTagFirstLabel=\u8BF7\u5148\u6DFB\u52A0\u4E00\u7BC7\u5E26\u6709\u6807\u7B7E [{tag}] \u7684\u6587\u7AE0 +searchLabel=\u641C\u7D22 +dynamicLabel=\u52A8\u6001 +exportSQLLabel=\u5BFC\u51FA SQL \u6587\u4EF6 +exportJSONLabel=\u5BFC\u51FA JSON \u6587\u4EF6 +exportHexoLabel=\u5BFC\u51FA Hexo \u6587\u4EF6 +footerContent1Label=\u9875\u811A\uFF1A +userAvatar1Label=\u5934\u50CF\uFF1A +staticErrorLabel=

Latke \u914D\u7F6E\u9519\u8BEF

\ +



\u8BF7\u6D4F\u89C8 Latke \u914D\u7F6E\u5256\u6790 \u4EE5\u89E3\u51B3\u8BE5\u95EE\u9898\u3002


+markdownHelpLabel=
\u6807\u9898
# \u4E00\u7EA7\u6807\u9898
#### \u56DB\u7EA7\u6807\u9898
\ +
\u94FE\u63A5
[\u94FE\u63A5\u6587\u5B57](\u94FE\u63A5\u5730\u5740)
\ +
\u5F3A\u8C03
*\u659C\u4F53* \u6216\u8005 _\u659C\u4F53_
**\u7C97\u4F53** \u6216\u8005 __\u7C97\u4F53__
***\u7C97\u4F53+\u659C\u4F53*** \u6216\u8005 ___\u7C97\u4F53+\u659C\u4F53___
\ +
\u5217\u8868
* \u65E0\u5E8F
1. \u6709\u5E8F
\ +
\u4EE3\u7801
`var markDown = undefined`
\ +
\u56FE\u7247
![alt text](/path/to/img.jpg 'Title')
\ +
\u5B8C\u6574\u8BED\u6CD5
+editType1Label=\u7F16\u8F91\u5668\u7C7B\u578B\uFF1A +typeLabel=\u7C7B\u578B +onlineVisitor1Label=\u5F53\u524D\u8BBF\u5BA2\uFF1A +noDataLabel=\u8BE5\u5217\u8868\u5F88\u61D2\uFF0C\u4EC0\u4E48\u90FD\u6CA1\u6709\u7559\u4E0B\u3002\u3002\u3002 +linkDescriptionLabel=\u94FE\u63A5\u63CF\u8FF0 +linkDescription1Label=\u63CF\u8FF0\uFF1A +descriptionLabel=\u63CF\u8FF0 +addressInvalidLabel=\u8BF7\u4EE5\u534F\u8BAE\u5F00\u5934\uFF0C\u5982: http:// +ToolLabel=\u5DE5\u5177 +titleAndContentLabel=\u6807\u9898+\u6B63\u6587 +titleAndAbstractLabel=\u6807\u9898+\u6458\u8981 +titleOnlyLabel=\u4EC5\u6807\u9898 +articleListDisplay1Label=\u6587\u7AE0\u5217\u8868\u663E\u793A\u65B9\u5F0F\uFF1A +checkingVersionLabel=\u6B63\u5728\u68C0\u67E5\u7248\u672C\u66F4\u65B0.... +upToDateLabel=\u60A8\u6B63\u5728\u4F7F\u7528\u6700\u65B0\u7684 Solo \u2728 +outOfDateLabel=\u60A8\u7684 Solo \u8FC7\u65F6\u4E86 \uD83D\uDE22\uFF0C\u8BF7\u4E0B\u8F7D\u6700\u65B0\u7248\u672C\uFF1A +aboutLabel=\u5173\u4E8E +aboutContentLabel=

Solo \ +\u662F\u4E00\u6B3E\u5C0F\u800C\u7F8E\u7684 Java \u535A\u5BA2\u7CFB\u7EDF\uFF0C\u7531 B3log \u5F00\u6E90\u793E\u533A\u5F00\u53D1\u5E76\u7EF4\u62A4\u3002

\ +

\u6211\u4EEC\u6B63\u5728\u6784\u5EFA\u72EC\u7ACB\u535A\u5BA2 + \u793E\u533A\u7684\u751F\u6001\uFF0C\u6B22\u8FCE\u52A0\u5165\u6211\u4EEC\uFF01

+confirmLabel=\u786E\u5B9A +adminConsoleLabel=\u540E\u53F0\u7BA1\u7406 +adminIndexLabel=\u540E\u53F0\u9996\u9875 +postArticleLabel=\u53D1\u5E03\u6587\u7AE0 +articleListLabel=\u6587\u7AE0\u7BA1\u7406 +commentListLabel=\u8BC4\u8BBA\u7BA1\u7406 +draftListLabel=\u8349\u7A3F\u5939 +userManageLabel=\u7528\u6237\u7BA1\u7406 +commonUserLabel=\u4E00\u822C\u7528\u6237 +visitorUserLabel=\u8BBF\u5BA2\u7528\u6237 +updateUserLabel=\u66F4\u65B0\u7528\u6237 +updateCategoryLabel=\u66F4\u65B0\u5206\u7C7B +linkManagementLabel=\u94FE\u63A5\u7BA1\u7406 +categoryListLabel=\u5206\u7C7B\u7BA1\u7406 +pluginMgmtLabel=\u63D2\u4EF6\u7BA1\u7406 +pluginNameLabel=\u63D2\u4EF6\u540D +versionLabel=\u7248\u672C +statusLabel=\u72B6\u6001 +enabledLabel=\u5DF2\u542F\u7528 +disabledLabel=\u5DF2\u7981\u7528 +enableLabel=\u542F\u7528 +disableLabel=\u7981\u7528 +settingLabel=\u8BBE\u7F6E +preferenceLabel=\u504F\u597D\u8BBE\u5B9A +localeString1Label=\u8BED\u8A00\uFF1A +timeZoneId1Label=\u65F6\u533A\uFF1A +adminLabel=\u7BA1\u7406 +roleLabel=\u89D2\u8272 +administratorLabel=\u7BA1\u7406\u5458 +logoutLabel=\u767B\u51FA +initLabel=\u521D\u59CB\u5316 +popTagsLabel=\u5206\u7C7B\u6807\u7B7E +tag1Label=\u6807\u7B7E\uFF1A +tags1Label=\u6807\u7B7E\uFF1A +tags1WithTips1Label=\u6807\u7B7E\uFF08\u4F7F\u7528\u82F1\u6587\u8F93\u5165\u72B6\u6001\u4E0B\u7684\u9017\u53F7\u8FDB\u884C\u5206\u9694\uFF09\uFF1A +recentArticlesLabel=\u6700\u65B0\u6587\u7AE0 +recentCommentsLabel=\u6700\u65B0\u8BC4\u8BBA +postCommentsLabel=\u53D1\u8868\u8BC4\u8BBA +mostCommentArticlesLabel=\u8BC4\u8BBA\u6700\u591A\u7684\u6587\u7AE0 +mostViewCountArticlesLabel=\u8BBF\u95EE\u6700\u591A\u7684\u6587\u7AE0 +em00Label=:smile: +em01Label=:joy: +em02Label=:stuck_out_tongue_winking_eye: +em03Label=:persevere: +em04Label=:sob: +em05Label=:cold_sweat: +em06Label=:rage: +em07Label=:triumph: +em08Label=:eyes: +em09Label=:scream: +em10Label=:sunglasses: +em11Label=:yum: +em12Label=:heart: +em13Label=:broken_heart: +em14Label=:smiling_imp: +linkLabel=\u53CB\u60C5\u94FE\u63A5 +sumLabel=\u5171 +pageLabel=\u9875 +commentLabel=\u8BC4\u8BBA +linkTitleLabel=\u94FE\u63A5\u6807\u9898 +linkTitle1Label=\u6807\u9898\uFF1A +updateLabel=\u66F4\u65B0 +removeLabel=\u5220\u9664 +putTopLabel=\u7F6E\u9876 +cancelPutTopLabel=\u53D6\u6D88\u7F6E\u9876 +downloadCountLabel=\u4E0B\u8F7D\u6B21\u6570 +sizeLabel=\u5927\u5C0F +uploadDateLabel=\u4E0A\u4F20\u65E5\u671F +downloadURLLabel=\u4E0B\u8F7D\u5730\u5740 +downloadLabel=\u4E0B\u8F7D +createDateLabel=\u521B\u5EFA\u65E5\u671F +updateDateLabel=\u66F4\u65B0\u65E5\u671F +dateLabel=\u65E5\u671F +titleLabel=\u6807\u9898 +title1Label=\u6807\u9898\uFF1A +content1Label=\u6B63\u6587\uFF1A +abstract1Label=\u6458\u8981\uFF1A +publishLabel=\u53D1\u5E03 +unPublishLabel=\u53D6\u6D88\u53D1\u5E03 +urlLabel=URL +url1Label=URL\uFF1A +addLinkLabel=\u6DFB\u52A0\u94FE\u63A5 +updateLinkLabel=\u66F4\u65B0\u94FE\u63A5 +archiveLabel=\u5B58\u6863 +archive1Label=\u5B58\u6863\uFF1A +yearLabel=\u5E74 +monthLabel=\u6708 +navMgmtLabel=\u5BFC\u822A\u7BA1\u7406 +navLabel=\u5BFC\u822A +openMethod1Label=\u9875\u9762\u6253\u5F00\u65B9\u5F0F\uFF1A +openMethodLabel=\u9875\u9762\u6253\u5F00\u65B9\u5F0F +linkEmptyLabel=\u94FE\u63A5\u4E0D\u80FD\u4E3A\u7A7A +targetSelfLabel=\u5728\u5F53\u524D\u7A97\u53E3/\u6846\u67B6\u6253\u5F00 +targetBlankLabel=\u5728\u65B0\u7A97\u53E3\u6253\u5F00 (_blank) +targetParentLabel=\u5728\u7236\u7A97\u53E3/\u6846\u67B6\u6253\u5F00 (_parent) +targetTopLabel=\u5728\u9876\u90E8\u6846\u67B6\u6253\u5F00\uFF08\u91CD\u7F6E\u6240\u6709\u6846\u67B6\uFF09 (_top) +othersLabel=\u5176\u4ED6 +fileListLabel=\u6587\u4EF6\u7BA1\u7406 +submitUploadLabel=\u4E0A\u4F20 +fileNameLabel=\u6587\u4EF6\u540D +paramSettingsLabel=\u53C2\u6570\u8BBE\u7F6E +configSettingsLabel=\u4FE1\u606F\u914D\u7F6E +skinLabel=\u76AE\u80A4\u7BA1\u7406 +signLabel=\u7B7E\u540D\u6863 +sign1Label=\u7B7E\u540D\u6863\uFF1A +noSignLabel=\u4E0D\u4F7F\u7528 +signIsNullLabel=\u8BE5\u7B7E\u540D\u6863\u4E3A\u7A7A +statisticLabel=\u535A\u5BA2\u7EDF\u8BA1 +viewLabel=\u6D4F\u89C8 +countLabel=\u7BC7 +viewCount1Label=\u6D4F\u89C8\u6B21\u6570\uFF1A +articleCount1Label=\u6587\u7AE0\u603B\u6570\uFF1A +commentCountLabel=\u8BC4\u8BBA\u6570 +commentCount1Label=\u8BC4\u8BBA\u603B\u6570\uFF1A +getDateLabel=\u83B7\u53D6\u65E5\u671F +selectDateLabel=\u9009\u62E9\u65E5\u671F +selectDate1Label=\u9009\u62E9\u65E5\u671F\uFF1A +selectLabel=\u9009\u62E9 +importLabel=\u5BFC\u5165 +userNameLabel=\u7528\u6237\u540D +userName1Label=\u7528\u6237\u540D\uFF1A +userLabel=\u7528\u6237 +userPassword1Label=\u5BC6\u7801\uFF1A +userPasswordLabel=\u5BC6\u7801 +userURL1Label=\u94FE\u63A5\uFF1A +userURLLabel=\u94FE\u63A5 +categoryLabel=\u5206\u7C7B +noticeBoard1Label=\u516C\u544A\uFF1A +noticeBoardLabel=\u516C\u544A +htmlhead1Label=HTML head\uFF1A +indexTagDisplayCntLabel=\u9996\u9875\u6807\u7B7E\u663E\u793A\u6570 +indexTagDisplayCnt1Label=\u9996\u9875\u6807\u7B7E\u663E\u793A\u6570\uFF1A +indexRecentArticleDisplayCntLabel=\u6700\u65B0\u6587\u7AE0\u663E\u793A\u6570\u76EE +indexRecentArticleDisplayCnt1Label=\u6700\u65B0\u6587\u7AE0\u663E\u793A\u6570\u76EE\uFF1A +indexRecentCommentDisplayCntLabel=\u6700\u65B0\u8BC4\u8BBA\u663E\u793A\u6570\u76EE +indexRecentCommentDisplayCnt1Label=\u6700\u65B0\u8BC4\u8BBA\u663E\u793A\u6570\u76EE\uFF1A +indexMostCommentArticleDisplayCntLabel=\u8BC4\u8BBA\u6700\u591A\u6587\u7AE0\u663E\u793A\u6570\u76EE +indexMostCommentArticleDisplayCnt1Label=\u8BC4\u8BBA\u6700\u591A\u6587\u7AE0\u663E\u793A\u6570\u76EE\uFF1A +indexMostViewArticleDisplayCntLabel=\u8BBF\u95EE\u6700\u591A\u6587\u7AE0\u663E\u793A\u6570\u76EE +indexMostViewArticleDisplayCnt1Label=\u8BBF\u95EE\u6700\u591A\u6587\u7AE0\u663E\u793A\u6570\u76EE\uFF1A +relevantArticlesDisplayCntLabel=\u76F8\u5173\u9605\u8BFB\u663E\u793A\u6570\u76EE +relevantArticlesDisplayCnt1Label=\u76F8\u5173\u9605\u8BFB\u663E\u793A\u6570\u76EE\uFF1A +randomArticlesDisplayCntLabel=\u968F\u673A\u9605\u8BFB\u663E\u793A\u6570\u76EE +randomArticlesDisplayCnt1Label=\u968F\u673A\u9605\u8BFB\u663E\u793A\u6570\u76EE\uFF1A +externalRelevantArticlesDisplayCntLabel=\u7AD9\u5916\u76F8\u5173\u9605\u8BFB\u663E\u793A\u6570\u76EE +externalRelevantArticlesDisplayCnt1Label=\u7AD9\u5916\u76F8\u5173\u9605\u8BFB\u663E\u793A\u6570\u76EE\uFF1A +windowSizeLabel=\u5206\u9875\u9875\u7801\u6700\u5927\u5BBD\u5EA6 +windowSize1Label=\u5206\u9875\u9875\u7801\u6700\u5927\u5BBD\u5EA6\uFF1A +pageSizeLabel=\u5206\u9875\u6BCF\u9875\u663E\u793A\u6587\u7AE0\u6570 +pageSize1Label=\u5206\u9875\u6BCF\u9875\u663E\u793A\u6587\u7AE0\u6570\uFF1A +blogTitle1Label=\u535A\u5BA2\u6807\u9898\uFF1A +blogSubtitle1Label=\u535A\u5BA2\u5B50\u6807\u9898\uFF1A +blogHost1Label=\u535A\u5BA2\u57DF\u540D\uFF1A +submmitCommentLabel=\u63D0\u4EA4\u8BC4\u8BBA +saveLabel=\u4FDD\u5B58 +tagLabel=\u6807\u7B7E +tagsLabel=\u6807\u7B7E +indexLabel=\u9996\u9875 +nextArticle1Label=\u65B0\u4E00\u7BC7\uFF1A +previousArticle1Label=\u65E7\u4E00\u7BC7\uFF1A +updatedLabel=\u6709\u66F4\u65B0\uFF01 +topArticleLabel=\u7F6E\u9876\uFF01 +previousPageLabel=\u4E0A\u4E00\u9875 +nextPagePabel=\u4E0B\u4E00\u9875 +firstPageLabel=\u7B2C\u4E00\u9875 +lastPageLabel=\u6700\u540E\u4E00\u9875 +returnTo1Label=\u8FD4\u56DE\uFF1A +previousStepLabel=\u4E0A\u4E00\u6B65 +nextStepLabel=\u4E0B\u4E00\u6B65 +atomLabel=Atom +relevantArticlesLabel=\u76F8\u5173\u9605\u8BFB +relevantArticles1Label=\u76F8\u5173\u9605\u8BFB\uFF1A +randomArticlesLabel=\u968F\u673A\u9605\u8BFB +randomArticles1Label=\u968F\u673A\u9605\u8BFB\uFF1A +externalRelevantArticlesLabel=\u7AD9\u5916\u76F8\u5173\u9605\u8BFB +externalRelevantArticles1Label=\u7AD9\u5916\u76F8\u5173\u9605\u8BFB\uFF1A +metaKeywords1Label=Meta Keywords: +metaDescription1Label=Meta Description: +removeUnusedTagsLabel=\u79FB\u9664\u672A\u4F7F\u7528\u6807\u7B7E +goTopLabel=\u9876\u90E8 +permalink1Label=\u94FE\u63A5\uFF1A +permalinkLabel=\u94FE\u63A5 +welcomeToSoloLabel=\u6B22\u8FCE\u4F7F\u7528 +killBrowserLabel=

\u8BA9\u6211\u4EEC\u653E\u5F03\u4F7F\u7528\u90A3\u4E9B\u8FC7\u65F6\u3001\u4E0D\u5B89\u5168\u7684\u6D4F\u89C8\u5668\u5427\uFF01

\u4E3A\u4E86\u8BA9\u6D4F\u89C8\u5668\u66F4\u597D\u7684\u53D1\u5C55\uFF0C\u4EBA\u7C7B\u66F4\u52A0\u7684\u8FDB\u6B65\uFF0C\u62E5\u6709\u66F4\u597D\u7684\u4F53\u9A8C\uFF0C\u8BA9\u6211\u4EEC\u653E\u5F03\u4F7F\u7528\u90A3\u4E9B\u8FC7\u65F6\u3001\u4E0D\u5B89\u5168\u7684\u6D4F\u89C8\u5668\u3002

\u60A8\u53EF\u4EE5\u4E0B\u8F7D\u6CE8\uFF1A\u5220\u9664 /js/common.js \u4E2D\u7684 Util.killIE(); \u53EF\u5BF9\u6240\u6709\u6D4F\u89C8\u5668\u8FDB\u884C\u652F\u6301\u3002
+closeLabel=\u5173\u95ED +closeForeverLabel=\u6C38\u4E45\u5173\u95ED +readmoreLabel=\u9605\u8BFB\u66F4\u591A\u00BB +readmore2Label=\u9605\u8BFB\u66F4\u591A +replyLabel=\u56DE\u590D\u00BB +homeLabel=\u9996\u9875 +enableArticleUpdateHint1Label=\u542F\u7528\u6587\u7AE0\u66F4\u65B0\u63D0\u793A\uFF1A +allowVisitDraftViaPermalink1Label=\u5141\u8BB8\u901A\u8FC7\u94FE\u63A5\u8BBF\u95EE\u8349\u7A3F\uFF1A +allowComment1Label=\u5141\u8BB8\u8BC4\u8BBA\uFF1A +allowCommentLabel=\u5141\u8BB8\u8BC4\u8BBA +feedOutputModel1Label=\u8BA2\u9605\u8F93\u51FA\u6A21\u5F0F\uFF1A +feedOutputCntLabel=\u8BA2\u9605\u8F93\u51FA\u6587\u7AE0\u6570 +feedOutputCnt1Label=\u8BA2\u9605\u8F93\u51FA\u6587\u7AE0\u6570\uFF1A +customVars1Label=\u81EA\u5B9A\u4E49\u6A21\u677F\u53D8\u91CF\uFF1A +abstractLabel=\u6458\u8981 +fullContentLabel=\u5168\u6587 +author1Label=\u4F5C\u8005\uFF1A +authorLabel=\u4F5C\u8005 +keyOfSolo1Label=B3log Key\uFF1A +articleLabel=\u6587\u7AE0 +tagArticlesLabel=\u6807\u7B7E\u6587\u7AE0\u5217\u8868 +dateArticlesLabel=\u5B58\u6863\u6587\u7AE0\u5217\u8868 +authorArticlesLabel=\u4F5C\u8005\u6587\u7AE0\u5217\u8868 +indexArticleLabel=\u9996\u9875\u6587\u7AE0\u5217\u8868 +userTemplatePageLabel=\u7528\u6237\u6A21\u7248\u9875\u9762 +allTagsLabel=\u6807\u7B7E\u5899 +customizedPageLabel=\u81EA\u5B9A\u4E49\u9875\u9762 +killBrowserPageLabel=Kill Browser Page +pageNumLabel=\u9875\u53F7 +articleViewPwdLabel=\u8BBF\u95EE\u5BC6\u7801 +articleViewPwd1Label=\u8BBF\u95EE\u5BC6\u7801\uFF1A +#### +forbiddenLabel=\u64CD\u4F5C\u88AB\u7981\u6B62\uFF01 +notFoundLabel=\u627E\u4E0D\u5230\uFF01 +unPulbishSuccLabel=\u53D6\u6D88\u53D1\u5E03\u6210\u529F +unPulbishFailLabel=\u53D6\u6D88\u53D1\u5E03\u5931\u8D25 +removeSuccLabel=\u5220\u9664\u6210\u529F +removeFailLabel=\u5220\u9664\u5931\u8D25 +putTopSuccLabel=\u7F6E\u9876\u6210\u529F +putTopFailLabel=\u7F6E\u9876\u5931\u8D25 +cancelTopSuccLabel=\u53D6\u6D88\u7F6E\u9876\u6210\u529F +cancelTopFailLabel=\u53D6\u6D88\u7F6E\u9876\u5931\u8D25 +addSuccLabel=\u6DFB\u52A0\u6210\u529F +addFailLabel=\u6DFB\u52A0\u5931\u8D25 +updateSuccLabel=\u66F4\u65B0\u6210\u529F +updateFailLabel=\u66F4\u65B0\u5931\u8D25 +setFailLabel=\u8BBE\u7F6E\u5931\u8D25 +setSuccLabel=\u8BBE\u7F6E\u6210\u529F +getFailLabel=\u83B7\u53D6\u5931\u8D25 +getSuccLabel=\u83B7\u53D6\u6210\u529F +noCommentLabel=\u6682\u65E0\u8BC4\u8BBA +inputErrorLabel=\u8F93\u5165\u9519\u8BEF\uFF01 +gotoLabel=\u8DF3\u8F6C +passwordNotMatchLabel=\u5BC6\u7801\u4E0D\u5339\u914D\uFF01 +notAllowCommentLabel=\u4E0D\u5141\u8BB8\u8BC4\u8BBA\uFF01 +nameTooLongLabel=\u59D3\u540D\u53EA\u80FD\u4E3A 2 \u5230 20 \u4E2A\u5B57\u7B26\uFF01 +categoryTooLongLabel=\u5206\u7C7B\u540D\u79F0\u53EA\u80FD\u4E3A 2 \u5230 32 \u4E2A\u5B57\u7B26\uFF01 +urlInvalidLabel=\u94FE\u63A5\u683C\u5F0F\u4E0D\u6B63\u786E\uFF01 +nonNegativeIntegerOnlyLabel=\u53EA\u80FD\u4E3A\u975E\u8D1F\u6574\u6570\uFF01 +commentContentCannotEmptyLabel=\u8BC4\u8BBA\u5185\u5BB9\u53EA\u80FD\u4E3A 2 \u5230 500 \u4E2A\u5B57\u7B26\uFF01 +loadingLabel=\u8F7D\u5165\u4E2D.... +titleEmptyLabel=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A\uFF01 +contentEmptyLabel=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A\uFF01 +orderEmptyLabel=\u5E8F\u53F7\u4E0D\u80FD\u4E3A\u7A7A\uFF01 +abstractEmptyLabel=\u6458\u8981\u4E0D\u80FD\u4E3A\u7A7A\uFF01 +tagsEmptyLabel=\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u8BF7\u52FF\u5305\u542B\u7B26\u53F7\uFF09\uFF01 +addressEmptyLabel=\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A\uFF01 +duplicatedPermalinkLabel=\u94FE\u63A5\u91CD\u590D\uFF01 +invalidPermalinkFormatLabel=\u975E\u6CD5\u7684\u94FE\u63A5\u683C\u5F0F\uFF01 +duplicatedUserNameLabel=\u7528\u6237\u540D\u91CD\u590D\uFF01 +refreshAndRetryLabel=\u8BF7\u5237\u65B0\u91CD\u8BD5\uFF01 +editorLeaveLabel=\u7F16\u8F91\u5668\u4E2D\u8FD8\u6709\u5185\u5BB9\uFF0C\u662F\u5426\u79BB\u5F00\uFF1F +editorPostLabel=\u7F16\u8F91\u5668\u4E2D\u8FD8\u6709\u5185\u5BB9\uFF0C\u662F\u5426\u6E05\u7A7A\uFF1F +changeRoleLabel=\u6539\u53D8\u89D2\u8272 +#### +confirmRemoveLabel=\u662F\u5426\u5220\u9664 +confirmInitLabel=\u786E\u5B9A\u8FDB\u884C\u521D\u59CB\u5316\u5417\uFF1F +###### Common ###### +mobileLabel=\u79FB\u52A8\u7248 +helloWorld.title=\u4E16\u754C\uFF0C\u4F60\u597D\uFF01 +helloWorld.content=Solo \u535A\u5BA2\u7CFB\u7EDF\u5DF2\u7ECF\u521D\u59CB\u5316\u5B8C\u6BD5\uFF0C\u53EF\u5728\u7BA1\u7406\u540E\u53F0 - \u5DE5\u5177 - \u504F\u597D\u8BBE\u5B9A\u4E2D\u8C03\u6574\u66F4\u591A\u7EC6\u8282\u8BBE\u7F6E\u3002\u5982\u679C\u9700\u8981\u5BFC\u5165\u5DF2\u6709\u535A\u5BA2\u6587\u7AE0\uFF0C\u8BF7\u53C2\u8003\u6587\u6863 [Hexo/Jekyll/Markdown \u6587\u4EF6\u5BFC\u5165](https://hacpai.com/article/1498490209748)\u3002\n\n\ +\u5BF9\u4E86\uFF0C\u51FA\u4E8E\u5B89\u5168\u8003\u8651\u8BF7\u5C3D\u5FEB\u5B8C\u6210\u5982\u4E0B\u64CD\u4F5C\uFF1A\n\n\ +1. \u4F7F\u7528 GitHub \u8D26\u53F7\u767B\u5F55[\u793E\u533A](https://hacpai.com)\n\ +2. \u5728\u793E\u533A[\u4E2A\u4EBA\u8BBE\u7F6E - B3](https://hacpai.com/settings/b3) \u4E2D\u66F4\u65B0 B3 Key\n\ +3. \u5728 Solo \u7BA1\u7406\u540E\u53F0 - \u5DE5\u5177 - \u7528\u6237\u7BA1\u7406\u4E2D\u4E5F\u8FDB\u884C\u540C\u6837\u7684 B3 Key \u66F4\u65B0\n\n\ +\u53E6\u5916\uFF0CSolo \u4F1A\u6BCF\u5929\u81EA\u52A8\u5BFC\u51FA\u4F60\u7684\u535A\u5BA2\u6587\u7AE0\u5230 GitHub \u4ED3\u5E93\uFF08[\u793A\u4F8B](https://github.com/88250/solo-blog)\uFF09\uFF1A\n\n\ +1. \u6709\u673A\u4F1A\u8BA9\u66F4\u591A\u4EBA\u770B\u5230\u4F60\u7684\u6587\u7AE0\n\ +2. \u81EA\u52A8\u5907\u4EFD\u6587\u7AE0\u6570\u636E\n\n\ +\u8BE5\u529F\u80FD\u9ED8\u8BA4\u662F\u5F00\u542F\u7684\uFF0C\u5982\u679C\u4E0D\u9700\u8981\u8BF7\u5230\u7BA1\u7406\u540E\u53F0 - \u5DE5\u5177 - \u504F\u597D\u8BBE\u5B9A - \u53C2\u6570\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002\u5F00\u542F\u65F6\u4F1A\u81EA\u52A8\u521B\u5EFA/\u66F4\u65B0 `solo-blog` \u4ED3\u5E93\uFF0C\u8BF7\u6CE8\u610F\u4F60\u6CA1\u6709\u8BE5\u540C\u540D\u4ED3\u5E93\u4EE5\u514D\u6570\u636E\u88AB\u8986\u76D6\u3002\n\n\ +\u6700\u540E\uFF0C\u5982\u679C\u4F60\u89C9\u5F97 Solo \u5F88\u8D5E\uFF0C\u8BF7\u5230[\u9879\u76EE\u4E3B\u9875](https://github.com/b3log/solo)\u7ED9\u9897\u661F\u9F13\u52B1\u4E00\u4E0B :heart: +helloWorld.comment.content=\u5199\u535A\u5BA2\u9700\u8981\u575A\u6301\uFF0C\u76F8\u4FE1\u79EF\u7D2F\u540E\u5FC5\u7136\u4F1A\u6709\u6536\u83B7\uFF0C\u6211\u4EEC\u4E00\u8D77\u52AA\u529B\u52A0\u6CB9 :smile: +articleContentPwd=\u8BE5\u6587\u7AE0\u5DF2\u7ECF\u52A0\u5BC6\u3002 +ok=\u786E\u5B9A diff --git a/src/main/resources/latke.properties b/src/main/resources/latke.properties new file mode 100644 index 00000000..553675f7 --- /dev/null +++ b/src/main/resources/latke.properties @@ -0,0 +1,33 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: B3log Latke configurations. +# Version: 1.6.0.2, Mar 19, 2019 +# Author: Liang Ding +# + +#### Server #### +# Browser visit protocol +serverScheme=http +#serverHost=localhost +#serverPort= + +#### Runtime Mode #### +runtimeMode=DEVELOPMENT +#runtimeMode=PRODUCTION diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 00000000..92503315 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,54 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Solo logging configurations. +# Version: 1.1.0.11, Apr 1, 2019 +# Author: Liang Ding +# Author: Lei Wang +# + +log4j.rootLogger=INFO,stdout + +log4j.logger.org.b3log.solo=INFO +log4j.logger.org.b3log.latke=WARN +log4j.logger.org.b3log.latke.util.freemarker.Templates=ERROR +log4j.logger.org.b3log.latke.repository.jdbc.util=WARN +log4j.logger.org.b3log.latke.util.Crypts=ERROR + +log4j.logger.org.eclipse.jetty=WARN +log4j.logger.freemarker=WARN +log4j.logger.com.mchange=WARN +log4j.logger.com.alibaba=WARN + +# Print only messages of level ERROR or above in the package noModule. +log4j.logger.noModule=ERROR + +# Console appender +log4j.appender.stdout.Encoding=UTF-8 +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%-5p]-[%d{yyyy-MM-dd HH:mm:ss}]-[%c:%L]: %m%n + +# File appender +#log4j.appender.file=org.apache.log4j.RollingFileAppender +#log4j.appender.file.File=solo.log +#log4j.appender.file.MaxFileSize=50MB +#log4j.appender.file.MaxBackupIndex=5 +#log4j.appender.file.layout=org.apache.log4j.PatternLayout +#log4j.appender.file.layout.ConversionPattern=[%-5p]-[%d{yyyy-MM-dd HH:mm:ss}]-[%c:%L]: %m%n diff --git a/src/main/resources/opensearch.xml b/src/main/resources/opensearch.xml new file mode 100644 index 00000000..3b9789de --- /dev/null +++ b/src/main/resources/opensearch.xml @@ -0,0 +1,13 @@ + + + + ${blogTitle} + ${blogSubtitle} + UTF-8 + https://static.b3log.org/images/brand/solo-32.png + + \ No newline at end of file diff --git a/src/main/resources/repository.json b/src/main/resources/repository.json new file mode 100644 index 00000000..b2bec6ef --- /dev/null +++ b/src/main/resources/repository.json @@ -0,0 +1,527 @@ +{ + "description": "Description of repository structures, for generation of the relational database table and persistence validation.", + "version": "3.2.1.7, Apr 19, 2019", + "authors": [ + "Liang Ding" + ], + "since": "0.4.0", + "repositories": [ + { + "name": "category", + "description": "分类表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "categoryTitle", + "description": "分类标题", + "type": "String", + "length": 64 + }, + { + "name": "categoryURI", + "description": "分类访问路径", + "type": "String", + "length": 255 + }, + { + "name": "categoryDescription", + "description": "分类描述", + "type": "String", + "length": 512 + }, + { + "name": "categoryOrder", + "description": "分类展现的排序", + "type": "int" + }, + { + "name": "categoryTagCnt", + "description": "分类下聚合的标签计数", + "type": "int" + } + ] + }, + { + "name": "category_tag", + "description": "分类-标签关联表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "category_oId", + "description": "分类 id", + "type": "String", + "length": 19 + }, + { + "name": "tag_oId", + "description": "标签 id", + "type": "String", + "length": 19 + } + ] + }, + { + "name": "archivedate", + "description": "存档日期表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "archiveTime", + "description": "存档日期时间,该月份第一天的时间戳", + "type": "long" + } + ] + }, + { + "name": "archivedate_article", + "description": "存档-文章关联表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "archiveDate_oId", + "description": "存档日期 id", + "type": "String", + "length": 19 + }, + { + "name": "article_oId", + "description": "文章 id", + "type": "String", + "length": 19 + } + ] + }, + { + "name": "comment", + "description": "评论表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "commentContent", + "description": "评论内容", + "type": "String", + "length": 2048 + }, + { + "name": "commentCreated", + "description": "评论时间戳", + "type": "long" + }, + { + "name": "commentName", + "description": "评论人名称", + "type": "String", + "length": 50 + }, + { + "name": "commentOnId", + "description": "评论的文章/页面的 id", + "type": "String", + "length": 19 + }, + { + "name": "commentSharpURL", + "description": "评论访问路径,带 # 锚点", + "type": "String", + "length": 255 + }, + { + "name": "commentThumbnailURL", + "description": "评论人头像图片链接地址", + "type": "String", + "length": 512 + }, + { + "name": "commentURL", + "description": "评论人链接地址", + "type": "String", + "length": "255" + }, + { + "name": "commentOriginalCommentId", + "description": "评论回复时原始的评论 id,即父评论 id", + "type": "String", + "length": 19, + "nullable": true + }, + { + "name": "commentOriginalCommentName", + "description": "评论回复时原始的评论人名称,即父评论人名称", + "type": "String", + "length": 50, + "nullable": true + } + ] + }, + { + "name": "link", + "description": "链接表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "linkAddress", + "description": "链接地址", + "type": "String", + "length": 255 + }, + { + "name": "linkDescription", + "description": "链接描述", + "type": "String", + "length": 255 + }, + { + "name": "linkOrder", + "description": "链接展现的排序", + "type": "int" + }, + { + "name": "linkTitle", + "description": "链接标题", + "type": "String", + "length": 255 + } + ] + }, + { + "name": "page", + "description": "自定义页面表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "pageOrder", + "description": "页面展现排序", + "type": "int" + }, + { + "name": "pagePermalink", + "description": "页面访问路径", + "type": "String", + "length": 255 + }, + { + "name": "pageTitle", + "description": "页面标题", + "type": "String", + "length": 255 + }, + { + "name": "pageOpenTarget", + "description": "页面打开方式", + "type": "String", + "length": 255 + }, + { + "name": "pageIcon", + "description": "页面展示用小图标", + "type": "String", + "length": 255 + } + ] + }, + { + "name": "plugin", + "description": "插件表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 128 + }, + { + "name": "author", + "description": "插件作者", + "type": "String", + "length": 2000 + }, + { + "name": "name", + "description": "插件名称", + "type": "String", + "length": 255 + }, + { + "name": "status", + "description": "插件状态,启用:ENABLED,禁用:DISABLED", + "type": "String", + "length": 10 + }, + { + "name": "version", + "description": "插件版本", + "type": "String", + "length": 10 + }, + { + "name": "setting", + "description": "插件配置,JSON 字符串", + "type": "String", + "length": 2000, + "nullable": true + } + ] + }, + { + "name": "tag", + "description": "标签表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "tagTitle", + "description": "标签标题", + "type": "String", + "length": 255 + } + ] + }, + { + "name": "tag_article", + "description": "标签-文章关联表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "article_oId", + "description": "文章 id", + "type": "String", + "length": 19 + }, + { + "name": "tag_oId", + "description": "标签 id", + "type": "String", + "length": 19 + } + ] + }, + { + "name": "user", + "description": "用户表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "userName", + "description": "用户名", + "type": "String", + "length": 255 + }, + { + "name": "userURL", + "description": "用户链接地址", + "type": "String", + "length": 255 + }, + { + "name": "userRole", + "description": "用户角色,管理员:adminRole,普通用户:defaultRole,访客用户:visitorRole", + "type": "String", + "length": 255 + }, + { + "name": "userAvatar", + "description": "用户头像图片链接地址", + "type": "String", + "length": 255 + }, + { + "name": "userB3Key", + "description": "B3log Key", + "type": "String", + "length": "64" + }, + { + "name": "userGitHubId", + "description": "用户 GitHub Open Id", + "type": "String", + "length": 32 + } + ] + }, + { + "name": "article", + "description": "文章表", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 19 + }, + { + "name": "articleTitle", + "description": "文章标题", + "type": "String", + "length": 255 + }, + { + "name": "articleAbstract", + "description": "文章摘要 Markdown", + "type": "String", + "length": 2048 + }, + { + "name": "articleAbstractText", + "description": "文章摘要纯文本", + "type": "String", + "length": 2048 + }, + { + "name": "articleTags", + "description": "文章标签,英文逗号分隔", + "type": "String", + "length": 2048 + }, + { + "name": "articleAuthorId", + "description": "文章作者 id", + "type": "String", + "length": 19 + }, + { + "name": "articleCommentCount", + "description": "文章评论计数", + "type": "int" + }, + { + "name": "articleViewCount", + "description": "文章浏览计数", + "type": "int" + }, + { + "name": "articleContent", + "description": "文章正文内容", + "type": "String", + "length": 1048576 + }, + { + "name": "articlePermalink", + "description": "文章访问路径", + "type": "String", + "length": 255 + }, + { + "name": "articlePutTop", + "description": "文章是否置顶", + "type": "boolean" + }, + { + "name": "articleCreated", + "description": "文章创建时间戳", + "type": "long" + }, + { + "name": "articleUpdated", + "description": "文章更新时间戳", + "type": "long" + }, + { + "name": "articleRandomDouble", + "description": "文章随机数,用于快速查询随机文章列表", + "type": "double" + }, + { + "name": "articleSignId", + "description": "文章关联的签名档 id", + "type": "String", + "length": 19 + }, + { + "name": "articleCommentable", + "description": "文章是否可以评论", + "type": "boolean" + }, + { + "name": "articleViewPwd", + "description": "文章浏览密码,留空为不设置访问密码", + "type": "String", + "legnth": 255 + }, + { + "name": "articleImg1URL", + "type": "String", + "length": 255, + "description": "文章首图地址" + }, + { + "name": "articleStatus", + "type": "int", + "description": "文章状态,0:已发布,1:草稿" + } + ] + }, + { + "name": "option", + "description": "配置项", + "keys": [ + { + "name": "oId", + "description": "主键", + "type": "String", + "length": 64 + }, + { + "name": "optionValue", + "description": "配置项值", + "type": "String", + "length": 10240 + }, + { + "name": "optionCategory", + "description": "配置项分类", + "type": "String", + "legnth": 20 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/solo.properties b/src/main/resources/solo.properties new file mode 100644 index 00000000..bfa2a423 --- /dev/null +++ b/src/main/resources/solo.properties @@ -0,0 +1,25 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Solo configurations. +# Version: 2.5.0.1, Apr 2, 2019 +# Author: Liang Ding +# + +faviconAPI=https://api.byi.pw/favicon?url= diff --git a/src/main/webapp/CHANGE_LOGS.html b/src/main/webapp/CHANGE_LOGS.html new file mode 100644 index 00000000..d6d5b550 --- /dev/null +++ b/src/main/webapp/CHANGE_LOGS.html @@ -0,0 +1,1007 @@ + + + + + + Solo Change Logs + + + + + +

Release 3.6.4 - Aug 19, 2019

+
    +
  • 12817 next 皮肤移动端图标没有居中 skin
  • +
  • 12842 文章更新时的界面优化 skin
  • +
  • 12843 皮肤 yilia 优化 skin
  • +
  • 12814 关闭内置 MD 引擎的 [TOC] 扩展 enhancement
  • +
  • 12826 订阅输出改进 enhancement
  • +
  • 12851 更新社区站点链接图标 enhancement
  • +
  • 12816 文章中有 GA 广告时会出现滚动条 bug
  • +
  • 12821 分类无法添加超过 4 个标签 bug
  • +
  • 12824 JDK11 插件加载报错 bug
  • +
  • 12838 同步 GitHub 仓库时没发布的草稿会同步到主页 bug
  • +
  • 12846 Bubble 皮肤头像问题 bug
  • +
  • 12850 编辑器表情问题 bug
  • +
+

Release 3.6.3 - Jul 13, 2019

+
    +
  • 12805 新皮肤 Bubble skin
  • +
  • 12799 标签文章数不计入草稿 enhancement
  • +
  • 12802 标签中含有#号时无法保存 enhancement
  • +
  • 12806 使用 MySQL 时不启用 SQL 导出功能 enhancement
  • +
  • 12810 移动端编辑器改进 enhancement
  • +
  • 12812 提升文章首图精度 enhancement
  • +
  • 12798 升级 MySQL 驱动 development
  • +
  • 12807 升级 Vditor 至 1.5.12 development
  • +
  • 12672 JDK11 插件加载报错 bug
  • +
  • 12813 在带中文的路径下登录报错 400 bug
  • +
+

Release 3.6.2 - Jun 7, 2019

+
    +
  • 12796 支持图表 feature
  • +
  • 12781 标签最长允许 16 字符 enhancement
  • +
  • 12782 修改用户名提示 enhancement
  • +
  • 12787 迁移历史表情图片 enhancement
  • +
  • 12788 改进 Markdown 代码块中的 Emoji 处理 enhancement
  • +
  • 12794 数据对象字段更新优化 development
  • +
+

Release 3.6.1 - May 21, 2019

+
    +
  • 12773 简化皮肤页脚 skin
  • +
  • 12768 放宽自定义导航链接校验 enhancement
  • +
  • 12769 改进标签文章列表排序 enhancement
  • +
  • 12770 导出 SQL 包后清理临时文件 enhancement
  • +
  • 12771 改进后台管理中的分类更新 enhancement
  • +
  • 12776 GitHub 导出同步后清理临时文件 enhancement
  • +
  • 12777 启动日志加入版本号、容器信息 enhancement
  • +
  • 12778 评论中的 Emoji 使用原始字符 enhancement
  • +
  • 12774 升级 npm 中 tar 和 extend 版本 development
  • +
+

Release 3.6.0 - Apr 19, 2019

+
    +
  • 12748 Casper 细节修改 skin
  • +
  • 12752 皮肤不显示访客用户 skin
  • +
  • 12747 log4j 默认日志配置不输出文件 enhancement
  • +
  • 12758 签名档内置模板变量 enhancement
  • +
  • 12759 我的开源页面排版推荐 enhancement
  • +
  • 12761 提高社区图床图片渲染质量 enhancement
  • +
  • 12763 移动端不显示看板娘 enhancement
  • +
  • 12764 去掉自定义页面 enhancement
  • +
  • 12749 清理 Gravatar 相关 development
  • +
+

Release 3.5.0 - Apr 1, 2019

+
    +
  • 12722 在设置中可选择语法高亮主题 feature
  • +
  • 12726 在设置中可配置移动端皮肤 skin
  • +
  • 12737 新皮肤 Casper skin
  • +
  • 12724 皮肤管理改进 enhancement
  • +
  • 12740 友好的报错提示 enhancement
  • +
  • 12741 Docker 镜像优化 enhancement
  • +
  • 12743 保存文章 401 提示 enhancement
  • +
  • 12746 默认不勾选推送到社区 enhancement
  • +
  • 12738 信任社区 SSL 证书 development
  • +
  • 12733 分类异步加载请求 404 bug
  • +
  • 12742 命令行参数 server_scheme 失效问题 bug
  • +
  • 12744 自动保存覆盖问题 bug
  • +
  • 12745 Error page loop /error/404 问题 bug
  • +
+

Release 3.4.0 - Mar 26, 2019

+
    +
  • 12676 博文定时同步 GitHub 仓库 feature
  • +
  • 12719 展示站点连接 feature
  • +
  • 12720 皮肤细节改进 skin
  • +
  • 12713 后台评论渲染和前端显示保持一致 enhancement
  • +
  • 12714 评论异步刷新时,甘特图等没有被渲染 enhancement
  • +
  • 12715 Docker 镜像地址更新 enhancement
  • +
  • 12716 更新博客域名提示 enhancement
  • +
  • 12717 修复升级程序问题 enhancement
  • +
  • 12718 以 Docker 作为主要的交付方式 enhancement
  • +
  • 12721 目录插件调整 enhancement
  • +
  • 12725 支持手动清理存档 enhancement
  • +
  • 12729 “我的开源”在当前页面打开 enhancement
  • +
  • 12730 Markdown 超链接 a 标签不加 id enhancement
  • +
  • 12734 用户名可包含横线 - enhancement
  • +
  • 12731 自定义页面评论 Markdown bug
  • +
+

Release 3.3.0 - Mar 18, 2019

+
    +
  • 12706 可配置 favicon 图标路径 feature
  • +
  • 12710 更好的支持数学公式、任务列表、流程图、时序图、甘特图 feature
  • +
  • 12705 Pinghsu 文章页面目录默认不显示 bug skin
  • +
  • 12698 Vditor 升级到 1.1.9 enhancement
  • +
  • 12699 移除开放注册开关 enhancement
  • +
  • 12702 Cookie 安全性增强 enhancement
  • +
  • 12703 Markdown 导出加上年月目录 enhancement
  • +
  • 12712 默认使用 MySQL enhancement
  • +
  • 12711 3.2.0 -> 3.3.0 皮肤升级 doc
  • +
  • 12708 webp 格式 iOS 问题 bug
  • +
+

Release 3.2.0 - Mar 5, 2019

+
    +
  • 12680 皮肤后台添加预览按钮 feature
  • +
  • 12684 皮肤 Pinghsu 页脚问题 skin
  • +
  • 12686 皮肤细节修改 skin
  • +
  • 12687 官方皮肤添补全原作地址 skin
  • +
  • 12679 Pinghsu 列表标题有更新重叠 enhancement
  • +
  • 12688 看版娘加新模型 enhancement
  • +
  • 12690 移除邮件相关功能 enhancement
  • +
  • 12691 社区回帖同步博客评论 enhancement
  • +
  • 12695 移除 fancybox 插件 enhancement
  • +
  • 12669 重构文章草稿、发布状态 development
  • +
  • 12682 移除 Servlet Filter development
  • +
  • 12683 latke.props 调整 development
  • +
  • 12681 看板娘提示修改 bug
  • +
  • 12689 皮肤 Finding 移动端目录跳转 bug bug
  • +
  • 12696 修复升级程序问题 bug
  • +
+

Release 3.1.0 - Feb 28, 2019

+
    +
  • 12647 支持跨版本升级 feature
  • +
  • 12651 皮肤功能补全 skin
  • +
  • 12673 新皮肤 Pinghsu skin
  • +
  • 12677 皮肤 favourite 的问题 skin
  • +
  • 12653 支持滑稽表情 enhancement
  • +
  • 12657 初始化改进 enhancement
  • +
  • 12666 页内锚点支持 enhancement
  • +
  • 12670 文章结构加入首图字段 enhancement
  • +
  • 12674 文章渲染加入分类字段 enhancement
  • +
  • 12675 皮肤中的分享修改 enhancement
  • +
  • 12678 文章结构加入摘要纯文本字段 enhancement
  • +
  • 12658 CSS 历史遗留问题修改 development
  • +
  • 12664 整理多语言配置 development
  • +
  • 12652 社区到博客的同步问题 bug
  • +
  • 12659 社区推送问题 bug
  • +
  • 12667 部署在非 ROOT 上登录问题 bug
  • +
  • 12668 随机文章会泄露浏览密码 bug
  • +
+

Release 3.0.0 - Feb 16, 2019

+
    +
  • 12556 内置 HTTPS+CDN 文件存储 feature
  • +
  • 12638 迁移到新编辑器 Vditor feature
  • +
  • 12629 社区同步幂等处理 enhancement
  • +
  • 12637 移除历史遗留的富文本编辑器 enhancement
  • +
  • 12639 移除历史遗留代码高亮库 enhancement
  • +
  • 12640 删除 MetaWeblog API enhancement
  • +
  • 12641 统一开始页 enhancement
  • +
  • 12645 前台登录评论及编辑器支持 enhancement
  • +
  • 12642 前端资源打包 development
  • +
  • 12644 管理后台模板重构 development
  • +
+

Release 2.9.9 - Jan 30, 2019

+
    +
  • 12514 同步 GitHub 仓库 feature
  • +
  • 12627 皮肤 Jane 增加目录 skin
  • +
  • 12625 改进导出功能导航 enhancement
  • +
  • 12630 仅支持 GitHub 登录 enhancement
  • +
  • 12631 Google SEO enhancement
  • +
  • 12633 重构数据统计计数 development
  • +
+

Release 2.9.8 - Jan 16, 2019

+
    +
  • 12497 支持阿里云 OSS feature
  • +
  • 12596 nijigen 添加文章目录 skin
  • +
  • 12599 皮肤 ease 加载更多问题 skin
  • +
  • 12610 发文章时可选择是否同步到社区 enhancement
  • +
  • 12611 移除内建的 Marked 改用 http-marked enhancement
  • +
  • 12613 升级 highlight.js enhancement
  • +
  • 12593 完善单元测试用例 development
  • +
  • 12597 统一请求取参方式 development
  • +
  • 12615 重构模板渲染构造器 development
  • +
  • 12622 ORM 层接口优化 development
  • +
  • 12606 邮件发送依赖库补全 bug
  • +
  • 12614 使用 Marked 时代码高亮问题 bug
  • +
  • 12619 用户注册问题 bug
  • +
  • 12620 手机端无法添加到桌面 bug
  • +
+

Release 2.9.7 - Dec 11, 2018

+
    +
  • 12535 支持配置自定义模板变量 feature
  • +
  • 12572 nijigen 皮肤继续优化 skin
  • +
  • 12584 新皮肤 Jane skin
  • +
  • 12566 优先使用皮肤内的登录、报错等模板 enhancement
  • +
  • 12583 调整列表分页 URL enhancement
  • +
  • 12587 缺失标签情况下也自动保存草稿 enhancement
  • +
  • 12577 调整请求 URL 风格 development
  • +
  • 12578 重构邮件发送 development
  • +
  • 12579 定时任务重构 development
  • +
  • 12580 后台控制器使用函数式路由 development
  • +
  • 12585 测试覆盖率使用 JaCoCo development
  • +
  • 12575 上传文件安全漏洞 bug
  • +
+

Release 2.9.6 - Nov 15, 2018

+
    +
  • 12550 调整默认皮肤为 nijigen skin
  • +
  • 12570 nijigen 皮肤修改 skin
  • +
  • 12543 禁止直接获取 robots.txt enhancement
  • +
  • 12546 改进 Bot 判断实现 enhancement
  • +
  • 12548 改进皮肤加载校验 enhancement
  • +
  • 12549 GitHub 登录安全改进 enhancement
  • +
  • 12551 七牛云域名配置校验 enhancement
  • +
  • 12554 可更新管理员邮箱 enhancement
  • +
  • 12558 回复提醒邮件模版支持博客地址变量 enhancement
  • +
  • 12561 皮肤多语言支持 ${servePath} enhancement
  • +
  • 12547 重写会话管理 development
  • +
  • 12568 重写初始化判断 development
  • +
  • 12542 搜索入口 XSS bug
  • +
  • 12555 MetaWeblog API 校验问题 bug
  • +
  • 12559 作者页 404 bug
  • +
  • 12564 修复验证码乱码 bug
  • +
+

Release 2.9.5 - Oct 10, 2018

+
    +
  • 12518 GitHub 登录支持 feature
  • +
  • 12508 皮肤 next 友情链接页面问题 skin
  • +
  • 12513 皮肤 Finding 修改 skin
  • +
  • 12524 皮肤 nijigen 添加 PJAX skin
  • +
  • 12503 前台皮肤 PAJX 优化 enhancement
  • +
  • 12512 Preference 性能优化 enhancement
  • +
  • 12517 支持通过用户名登录 enhancement
  • +
  • 12520 验证码增强 enhancement
  • +
  • 12521 看版娘浏览器不兼容时隐藏 enhancement
  • +
  • 12523 看板娘可以拖拽 enhancement
  • +
  • 12527 细分 401 及 403 enhancement
  • +
  • 12530 Feed 地址调整 enhancement
  • +
  • 12531 重写皮肤加载机制 enhancement
  • +
  • 12515 后端代码重构 development
  • +
  • 12525 使用 HikariCP 作为数据库连接池 development
  • +
  • 12534 升级 gulp development
  • +
  • 12519 分类 URI 编码问题 bug
  • +
  • 12522 编辑器乱码 bug
  • +
  • 12533 PJAX 问题 bug
  • +
+

Release 2.9.4 - Sep 16, 2018

+
    +
  • 12494 新皮肤 nijigen skin
  • +
  • 12486 Next 皮肤调整 enhancement
  • +
  • 12489 加强文章标签格式校验 enhancement
  • +
  • 12492 验证码改进 enhancement
  • +
  • 12495 skins 作为 git 子模块引入 development
  • +
  • 12507 删除 JDBC 连接池配置项 development
  • +
  • 12509 重构数据库 development
  • +
  • 12510 支持 MySQL 8 development
  • +
  • 12500 友情链接管理问题 bug
  • +
+

Release 2.9.3 - Aug 23, 2018

+
    +
  • 12478 可配置移动端皮肤 feature
  • +
  • 12481 使用 CDN 加速看板娘 enhancement
  • +
  • 12479 工具类重构 development
  • +
+

Release 2.9.2 - Jul 26, 2018

+
    +
  • 12472 看板娘插件 feature
  • +
  • 12474 数学公式支持改进 enhancement
  • +
  • 12477 调整访客数刷新间隔为 10 分钟 enhancement
  • +
+

Release 2.9.1 - Jun 27, 2018

+ +

Release 2.9.0 - May 17, 2018

+ +

Release 2.8.0 - Apr 17, 2018

+ +

Release 2.7.0 - Mar 7, 2018

+ +

Release 2.6.0 - Feb 1, 2018

+ +

Release 2.5.0 - Dec 20, 2017

+ +

Release 2.4.0 - Sep 22, 2017

+ +

Release 2.3.0 - Aug 31, 2017

+ +

Release 2.2.0 - Jul 10, 2017

+ +

Release 2.1.0 - May 25, 2017

+ +

Release 2.0.0 - Apr 14, 2017

+ +

Release 1.9.0 - Feb 21, 2017

+ +

Release 1.8.0 - Jan 23, 2017

+ +

Release 1.7.0 - Nov 9, 2016

+ +

Release 1.6.0 - Sep 8, 2016

+ +

Release 1.5.0 - Aug 10, 2016

+ +

Release 1.4.0 - Jun 28, 2016

+ +

Release 1.3.0 - Dec 19, 2015

+ +

Release 1.2.0 - Dec 2, 2015

+ +

Release 1.1.0 - Oct 1, 2015

+ +

Release 1.0.0 - Sep 16, 2015

+ +

Release 0.6.9 - Jun 28, 2015

+ +

Release 0.6.8 - Mar 24, 2015

+ +

Release 0.6.7 - Oct 16, 2014

+ +

Release 0.6.6 - Apr 26, 2014

+ +

Release 0.6.5 - Nov 1, 2013

+ +

Release 0.6.1 - Aug 25, 2013

+ +

Release 0.6.0 - Apr 26, 2013

+ +

Release 0.5.6 - Feb 19, 2012

+ +

Release 0.5.5 - Nov 24, 2012

+ + +

Release 0.5.0 - Aug 25, 2012

+ +

Release 0.4.6 - Jul 1, 2012

+ +

Release 0.4.5 - Jun 1, 2012

+ +

Release 0.4.1 - Apr 25, 2012

+ +

Release 0.4.0 - Feb 19, 2012

+ +

Release 0.3.5 - Oct 22, 2011

+ +

Release 0.3.1 - Oct 1, 2011

+ + +

Release 0.3.0 - Aug 20, 2011

+ + +

Release 0.2.5 - Apr 30, 2011

+ + +

Release 0.2.5 - Feb 13, 2011

+ + +

Release 0.2.1 - Dec 1, 2010

+ + +

Release 0.2.0 - Nov 12, 2010

+ + +

Release 0.1.1 - Oct 26, 2010

+ + + diff --git a/src/main/webapp/WEB-INF/static-resources.xml b/src/main/webapp/WEB-INF/static-resources.xml new file mode 100644 index 00000000..7eb81b75 --- /dev/null +++ b/src/main/webapp/WEB-INF/static-resources.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ade1e851 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,66 @@ + + + + + + org.b3log.solo.SoloServletListener + + + + + 60 + + + + + DispatcherServlet + org.b3log.latke.servlet.DispatcherServlet + 2 + + + DispatcherServlet + /* + + + + + 404 + /error/404 + + + 401 + /error/401 + + + 403 + /error/403 + + + 500 + /error/500 + + + diff --git a/src/main/webapp/admin/admin-about.ftl b/src/main/webapp/admin/admin-about.ftl new file mode 100644 index 00000000..9e94fa90 --- /dev/null +++ b/src/main/webapp/admin/admin-about.ftl @@ -0,0 +1,84 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
+
+

${aboutLabel}

+
+
+ +
+
+ ${checkingVersionLabel} +
+ + +
+ + ${aboutContentLabel} +
+ +
+

+
+
+ +
+
+

❤️ 欢迎成为我们的赞助者

+
+
+ B3log 开源组织旗下包含 + Symphony、 + Solo、 + Pipe、 + Wide、 + Latke、 + Vditor、 + Gulu 等一系列开源项目。随着项目规模的增长,我们需要有相应的资金支持才能持续项目的维护和开发。 +

+ 如果你觉得 Solo 还算好用,可通过支付宝对我们进行赞助,谢谢 🙏 +

+
+ +
+
+
    +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-article-list.ftl b/src/main/webapp/admin/admin-article-list.ftl new file mode 100644 index 00000000..1166b10c --- /dev/null +++ b/src/main/webapp/admin/admin-article-list.ftl @@ -0,0 +1,31 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-article.ftl b/src/main/webapp/admin/admin-article.ftl new file mode 100644 index 00000000..facbeb67 --- /dev/null +++ b/src/main/webapp/admin/admin-article.ftl @@ -0,0 +1,96 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + + + + + + +
    + +   + + + +
    +
    +
    +
    + + + +
    +
    +
    +${plugins} \ No newline at end of file diff --git a/src/main/webapp/admin/admin-category-list.ftl b/src/main/webapp/admin/admin-category-list.ftl new file mode 100644 index 00000000..37aaf143 --- /dev/null +++ b/src/main/webapp/admin/admin-category-list.ftl @@ -0,0 +1,39 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +
    +
    +
    +
    + + + + + + + + + +
    + +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-comment-list.ftl b/src/main/webapp/admin/admin-comment-list.ftl new file mode 100644 index 00000000..140361ae --- /dev/null +++ b/src/main/webapp/admin/admin-comment-list.ftl @@ -0,0 +1,25 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +
    +
    +
    +${plugins} \ No newline at end of file diff --git a/src/main/webapp/admin/admin-draft-list.ftl b/src/main/webapp/admin/admin-draft-list.ftl new file mode 100644 index 00000000..83e0b59a --- /dev/null +++ b/src/main/webapp/admin/admin-draft-list.ftl @@ -0,0 +1,27 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +
    +
    +
    +
    +
    +${plugins} \ No newline at end of file diff --git a/src/main/webapp/admin/admin-index.ftl b/src/main/webapp/admin/admin-index.ftl new file mode 100644 index 00000000..3f30a4d2 --- /dev/null +++ b/src/main/webapp/admin/admin-index.ftl @@ -0,0 +1,199 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/macro-common_head.ftl"/> + + + + <@head title="${adminConsoleLabel} - ${blogTitle}"> + + + + + +
    ${loadingLabel}
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + <#if "" == miniPostfix> + + + + + + + + + + + + + + + + + + + + <#else> + + + <#include "admin-label.ftl"> + ${plugins} + + + diff --git a/src/main/webapp/admin/admin-label.ftl b/src/main/webapp/admin/admin-label.ftl new file mode 100644 index 00000000..52382ef3 --- /dev/null +++ b/src/main/webapp/admin/admin-label.ftl @@ -0,0 +1,103 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/label.ftl"/> + \ No newline at end of file diff --git a/src/main/webapp/admin/admin-link-list.ftl b/src/main/webapp/admin/admin-link-list.ftl new file mode 100644 index 00000000..cb3c9f43 --- /dev/null +++ b/src/main/webapp/admin/admin-link-list.ftl @@ -0,0 +1,46 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    + +
    +
    +
    +${addLinkLabel} + + + + + +

    + +
    +
    + +${plugins} \ No newline at end of file diff --git a/src/main/webapp/admin/admin-main.ftl b/src/main/webapp/admin/admin-main.ftl new file mode 100644 index 00000000..550a990f --- /dev/null +++ b/src/main/webapp/admin/admin-main.ftl @@ -0,0 +1,22 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-others.ftl b/src/main/webapp/admin/admin-others.ftl new file mode 100644 index 00000000..9caeaadf --- /dev/null +++ b/src/main/webapp/admin/admin-others.ftl @@ -0,0 +1,47 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    + +
    +
    +
    + + +
    +
    + <#if supportExport> + + + + +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-page-list.ftl b/src/main/webapp/admin/admin-page-list.ftl new file mode 100644 index 00000000..c215e233 --- /dev/null +++ b/src/main/webapp/admin/admin-page-list.ftl @@ -0,0 +1,55 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-plugin-list.ftl b/src/main/webapp/admin/admin-plugin-list.ftl new file mode 100644 index 00000000..7a1829f1 --- /dev/null +++ b/src/main/webapp/admin/admin-plugin-list.ftl @@ -0,0 +1,27 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +
    +
    +
    +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-plugin-setting.ftl b/src/main/webapp/admin/admin-plugin-setting.ftl new file mode 100644 index 00000000..55ec3206 --- /dev/null +++ b/src/main/webapp/admin/admin-plugin-setting.ftl @@ -0,0 +1,52 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + + + + + + diff --git a/src/main/webapp/admin/admin-preference.ftl b/src/main/webapp/admin/admin-preference.ftl new file mode 100644 index 00000000..5fee5e39 --- /dev/null +++ b/src/main/webapp/admin/admin-preference.ftl @@ -0,0 +1,201 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    + +
    +
    +
    + +
    + + + + + + + + + + + + + + + +

    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + +
    + + + + + +

    + +
    +
    +
    +${plugins} diff --git a/src/main/webapp/admin/admin-theme-list.ftl b/src/main/webapp/admin/admin-theme-list.ftl new file mode 100644 index 00000000..28603211 --- /dev/null +++ b/src/main/webapp/admin/admin-theme-list.ftl @@ -0,0 +1,27 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + +${plugins} diff --git a/src/main/webapp/admin/admin-user-list.ftl b/src/main/webapp/admin/admin-user-list.ftl new file mode 100644 index 00000000..b90c90f6 --- /dev/null +++ b/src/main/webapp/admin/admin-user-list.ftl @@ -0,0 +1,39 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
    +
    +
    +
    +
    +
    +
    + + + + + + + + +

    + +
    +
    +${plugins} diff --git a/src/main/webapp/admin/article-pwd.ftl b/src/main/webapp/admin/article-pwd.ftl new file mode 100644 index 00000000..b90ae21e --- /dev/null +++ b/src/main/webapp/admin/article-pwd.ftl @@ -0,0 +1,39 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/macro-common_page.ftl"> + +<@commonPage "${articleViewPwdLabel}"> +

    +${articleTitle} +

    +


    +
    + + + +
    + <#if msg??> + ${msg} + + +
    +
    +


    + \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/kill-browser.ftl b/src/main/webapp/common-template/bkup/kill-browser.ftl new file mode 100644 index 00000000..79544ebc --- /dev/null +++ b/src/main/webapp/common-template/bkup/kill-browser.ftl @@ -0,0 +1,42 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_page.ftl"> + +<@commonPage "Kill IE!"> +
    + ${killBrowserLabel} +
    +   + +
    + Kill IE6 +
    + + \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/label.ftl b/src/main/webapp/common-template/bkup/label.ftl new file mode 100644 index 00000000..8c1fc173 --- /dev/null +++ b/src/main/webapp/common-template/bkup/label.ftl @@ -0,0 +1,30 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/macro-comment_script.ftl b/src/main/webapp/common-template/bkup/macro-comment_script.ftl new file mode 100644 index 00000000..4734c10d --- /dev/null +++ b/src/main/webapp/common-template/bkup/macro-comment_script.ftl @@ -0,0 +1,53 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#macro comment_script oId commentable> +<#if isLoggedIn && commentable> +
    +
    +
    +
    +
    +
    + + ${cancelLabel} + + +
    +
    +
    + + + \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/macro-common_head.ftl b/src/main/webapp/common-template/bkup/macro-common_head.ftl new file mode 100644 index 00000000..1389c88e --- /dev/null +++ b/src/main/webapp/common-template/bkup/macro-common_head.ftl @@ -0,0 +1 @@ +<#-- Solo - A small and beautiful blogging system written in Java. Copyright (c) 2010-present, b3log.org This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> <#macro head title description=''> ${title} <#if description != ''> <#elseif metaDescription??> <#else> <#if !blogSubtitle??> <#assign blogSubtitle = ''> <#if metaKeywords??> <#if paginationCurrentPageNum??> <#if paginationCurrentPageNum == 1> <#else> <#else> <#nested> <#if htmlHead??> ${htmlHead} \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/macro-common_page.ftl b/src/main/webapp/common-template/bkup/macro-common_page.ftl new file mode 100644 index 00000000..72f5fdc1 --- /dev/null +++ b/src/main/webapp/common-template/bkup/macro-common_page.ftl @@ -0,0 +1,51 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_head.ftl"/> +<#macro commonPage title> + + + + <#if !blogTitle??> + <#assign blogTitle = "Solo"> + + <@head title="${title} - ${blogTitle}"> + + + + + +
    +
    +
    +
    + <#nested> +
    +
    +
    +
    + +
    +
    + + + diff --git a/src/main/webapp/common-template/bkup/macro-user_site.ftl b/src/main/webapp/common-template/bkup/macro-user_site.ftl new file mode 100644 index 00000000..5b57f75c --- /dev/null +++ b/src/main/webapp/common-template/bkup/macro-user_site.ftl @@ -0,0 +1,176 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#macro userSite dir> +<#if usite??> + title<#else>aria-label="https://hacpai.com/member/${adminUser.userName}" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" + target="_blank" rel="noopener nofollow"> + + + + + + + + <#if usite.usiteGitHub != ''> + title<#else>aria-label="https://github.com/${usite.usiteGitHub}" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" + target="_blank" rel="noopener nofollow"> + + + + + + + <#if usite.usiteStackOverflow != ''> + title<#else>aria-label="https://stackoverflow.com/users/${usite.usiteStackOverflow}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteDribbble != ''> + title<#else>aria-label="https://dribbble.com/${usite.usiteDribbble}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteBehance != ''> + title<#else>aria-label="https://www.behance.net/${usite.usiteBehance}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteTwitter != ''> + title<#else>aria-label="https://twitter.com/${usite.usiteTwitter}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteFacebook != ''> + title<#else>aria-label="https://www.facebook.com/${usite.usiteFacebook}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteInstagram != ''> + title<#else>aria-label="https://www.instagram.com/${usite.usiteInstagram}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteMedium != ''> + title<#else>aria-label="https://medium.com/@${usite.usiteMedium}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteLinkedIn != ''> + title<#else>aria-label="https://www.linkedin.com/in/${usite.usiteLinkedIn}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteTelegram != ''> + title<#else>aria-label="https://telegram.me/${usite.usiteTelegram}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteWeiBo != ''> + title<#else>aria-label="https://weibo.com/${usite.usiteWeiBo}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + + + + <#if usite.usiteZhiHu != ''> + title<#else>aria-label="https://www.zhihu.com/people/${usite.usiteZhiHu}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteQQ != ''> + title<#else>aria-label="${usite.usiteQQ}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteWeChat != ''> + title<#else>aria-label="${usite.usiteWeChat}"> + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/search.ftl b/src/main/webapp/common-template/bkup/search.ftl new file mode 100644 index 00000000..364cec8b --- /dev/null +++ b/src/main/webapp/common-template/bkup/search.ftl @@ -0,0 +1,110 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_head.ftl"/> + + + + <@head title="${searchLabel} - ${blogTitle}${searchLabel}"> + + + + +
    + Solo +
    + + +
    + + <#if isLoggedIn> + ${adminLabel}   + ${logoutLabel} + <#else> + ${startToUseLabel} + + +
    + + + +
    + +
    + + diff --git a/src/main/webapp/common-template/bkup/share.ftl b/src/main/webapp/common-template/bkup/share.ftl new file mode 100644 index 00000000..1015ff2f --- /dev/null +++ b/src/main/webapp/common-template/bkup/share.ftl @@ -0,0 +1,49 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/start.ftl b/src/main/webapp/common-template/bkup/start.ftl new file mode 100644 index 00000000..993fa3b9 --- /dev/null +++ b/src/main/webapp/common-template/bkup/start.ftl @@ -0,0 +1,65 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_page.ftl"> + +<@commonPage "${welcomeToSoloLabel}!"> +

    + ${welcomeToSoloLabel} + +  Solo + +

    + +
    +
    + +
    +
    +
    + 查看 GitHub 数据使用说明 +
    +
      +
    • 获取用户名、头像等用于初始化
    • +
    • 获取公开仓库信息用于展示
    • +
    • 不会对你的已有数据进行写入
    • +
    +
    + +
    + + + diff --git a/src/main/webapp/common-template/bkup/toc.ftl b/src/main/webapp/common-template/bkup/toc.ftl new file mode 100644 index 00000000..cf4f74aa --- /dev/null +++ b/src/main/webapp/common-template/bkup/toc.ftl @@ -0,0 +1,26 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
      + <#list article.articleToC as item> +
    • + ${item.text} +
    • + +
    \ No newline at end of file diff --git a/src/main/webapp/common-template/bkup/top-bar.ftl b/src/main/webapp/common-template/bkup/top-bar.ftl new file mode 100644 index 00000000..bc6059a6 --- /dev/null +++ b/src/main/webapp/common-template/bkup/top-bar.ftl @@ -0,0 +1,107 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + +
    +
    + + Solo + +  ${onlineVisitor1Label}${onlineVisitorCnt} + + <#if isLoggedIn> + ${userName} + <#if !isVisitor> + + ${adminLabel} + + + ${logoutLabel} + <#else> + ${startToUseLabel} + + <#if isMobileRequest> + ${mobileLabel} + + + +
    +
    \ No newline at end of file diff --git a/src/main/webapp/common-template/kill-browser.ftl b/src/main/webapp/common-template/kill-browser.ftl new file mode 100644 index 00000000..79544ebc --- /dev/null +++ b/src/main/webapp/common-template/kill-browser.ftl @@ -0,0 +1,42 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_page.ftl"> + +<@commonPage "Kill IE!"> +
    + ${killBrowserLabel} +
    +   + +
    + Kill IE6 +
    + + \ No newline at end of file diff --git a/src/main/webapp/common-template/label.ftl b/src/main/webapp/common-template/label.ftl new file mode 100644 index 00000000..8c1fc173 --- /dev/null +++ b/src/main/webapp/common-template/label.ftl @@ -0,0 +1,30 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + \ No newline at end of file diff --git a/src/main/webapp/common-template/macro-comment_script.ftl b/src/main/webapp/common-template/macro-comment_script.ftl new file mode 100644 index 00000000..4734c10d --- /dev/null +++ b/src/main/webapp/common-template/macro-comment_script.ftl @@ -0,0 +1,53 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#macro comment_script oId commentable> +<#if isLoggedIn && commentable> +
    +
    +
    +
    +
    +
    + + ${cancelLabel} + + +
    +
    +
    + + + \ No newline at end of file diff --git a/src/main/webapp/common-template/macro-common_head.ftl b/src/main/webapp/common-template/macro-common_head.ftl new file mode 100644 index 00000000..1389c88e --- /dev/null +++ b/src/main/webapp/common-template/macro-common_head.ftl @@ -0,0 +1 @@ +<#-- Solo - A small and beautiful blogging system written in Java. Copyright (c) 2010-present, b3log.org This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> <#macro head title description=''> ${title} <#if description != ''> <#elseif metaDescription??> <#else> <#if !blogSubtitle??> <#assign blogSubtitle = ''> <#if metaKeywords??> <#if paginationCurrentPageNum??> <#if paginationCurrentPageNum == 1> <#else> <#else> <#nested> <#if htmlHead??> ${htmlHead} \ No newline at end of file diff --git a/src/main/webapp/common-template/macro-common_page.ftl b/src/main/webapp/common-template/macro-common_page.ftl new file mode 100644 index 00000000..72f5fdc1 --- /dev/null +++ b/src/main/webapp/common-template/macro-common_page.ftl @@ -0,0 +1,51 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_head.ftl"/> +<#macro commonPage title> + + + + <#if !blogTitle??> + <#assign blogTitle = "Solo"> + + <@head title="${title} - ${blogTitle}"> + + + + + +
    +
    +
    +
    + <#nested> +
    +
    +
    +
    + +
    +
    + + + diff --git a/src/main/webapp/common-template/macro-user_site.ftl b/src/main/webapp/common-template/macro-user_site.ftl new file mode 100644 index 00000000..5b57f75c --- /dev/null +++ b/src/main/webapp/common-template/macro-user_site.ftl @@ -0,0 +1,176 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#macro userSite dir> +<#if usite??> + title<#else>aria-label="https://hacpai.com/member/${adminUser.userName}" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" + target="_blank" rel="noopener nofollow"> + + + + + + + + <#if usite.usiteGitHub != ''> + title<#else>aria-label="https://github.com/${usite.usiteGitHub}" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" + target="_blank" rel="noopener nofollow"> + + + + + + + <#if usite.usiteStackOverflow != ''> + title<#else>aria-label="https://stackoverflow.com/users/${usite.usiteStackOverflow}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteDribbble != ''> + title<#else>aria-label="https://dribbble.com/${usite.usiteDribbble}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteBehance != ''> + title<#else>aria-label="https://www.behance.net/${usite.usiteBehance}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteTwitter != ''> + title<#else>aria-label="https://twitter.com/${usite.usiteTwitter}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteFacebook != ''> + title<#else>aria-label="https://www.facebook.com/${usite.usiteFacebook}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteInstagram != ''> + title<#else>aria-label="https://www.instagram.com/${usite.usiteInstagram}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteMedium != ''> + title<#else>aria-label="https://medium.com/@${usite.usiteMedium}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteLinkedIn != ''> + title<#else>aria-label="https://www.linkedin.com/in/${usite.usiteLinkedIn}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteTelegram != ''> + title<#else>aria-label="https://telegram.me/${usite.usiteTelegram}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteWeiBo != ''> + title<#else>aria-label="https://weibo.com/${usite.usiteWeiBo}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + + + + <#if usite.usiteZhiHu != ''> + title<#else>aria-label="https://www.zhihu.com/people/${usite.usiteZhiHu}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteQQ != ''> + title<#else>aria-label="${usite.usiteQQ}" + target="_blank" + class="<#if dir!=''>vditor-tooltipped__${dir} vditor-tooltipped user__site" rel="noopener nofollow"> + + + + + + <#if usite.usiteWeChat != ''> + title<#else>aria-label="${usite.usiteWeChat}"> + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/common-template/search.ftl b/src/main/webapp/common-template/search.ftl new file mode 100644 index 00000000..364cec8b --- /dev/null +++ b/src/main/webapp/common-template/search.ftl @@ -0,0 +1,110 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_head.ftl"/> + + + + <@head title="${searchLabel} - ${blogTitle}${searchLabel}"> + + + + +
    + Solo +
    + + +
    + + <#if isLoggedIn> + ${adminLabel}   + ${logoutLabel} + <#else> + ${startToUseLabel} + + +
    + + + +
    + +
    + + diff --git a/src/main/webapp/common-template/share.ftl b/src/main/webapp/common-template/share.ftl new file mode 100644 index 00000000..1015ff2f --- /dev/null +++ b/src/main/webapp/common-template/share.ftl @@ -0,0 +1,49 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + \ No newline at end of file diff --git a/src/main/webapp/common-template/start.ftl b/src/main/webapp/common-template/start.ftl new file mode 100644 index 00000000..e1e82a0f --- /dev/null +++ b/src/main/webapp/common-template/start.ftl @@ -0,0 +1,57 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "macro-common_page.ftl"> + +<@commonPage "${welcomeToSoloLabel}!"> +

    + 请使用 + Github账号 + 登入我的博客,参与互动! +

    + +
    +
    + +
    +
    + 查看 GitHub 数据使用说明 +
    +
      +
    • 获取用户名、头像等用于初始化
    • +
    • 获取公开仓库信息用于展示
    • +
    • 不会对你的已有数据进行写入
    • +
    +
    +
    + + + \ No newline at end of file diff --git a/src/main/webapp/common-template/toc.ftl b/src/main/webapp/common-template/toc.ftl new file mode 100644 index 00000000..cf4f74aa --- /dev/null +++ b/src/main/webapp/common-template/toc.ftl @@ -0,0 +1,26 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +
      + <#list article.articleToC as item> +
    • + ${item.text} +
    • + +
    \ No newline at end of file diff --git a/src/main/webapp/common-template/top-bar.ftl b/src/main/webapp/common-template/top-bar.ftl new file mode 100644 index 00000000..bc6059a6 --- /dev/null +++ b/src/main/webapp/common-template/top-bar.ftl @@ -0,0 +1,107 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + +
    +
    + + Solo + +  ${onlineVisitor1Label}${onlineVisitorCnt} + + <#if isLoggedIn> + ${userName} + <#if !isVisitor> + + ${adminLabel} + + + ${logoutLabel} + <#else> + ${startToUseLabel} + + <#if isMobileRequest> + ${mobileLabel} + + + +
    +
    \ No newline at end of file diff --git a/src/main/webapp/error/401.ftl b/src/main/webapp/error/401.ftl new file mode 100644 index 00000000..f41e046f --- /dev/null +++ b/src/main/webapp/error/401.ftl @@ -0,0 +1,28 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/macro-common_page.ftl"> + +<@commonPage "401 Unauthorized!"> +

    401 Unauthorized!

    +401 +
    + Please start or return to Index. +
    + diff --git a/src/main/webapp/error/403.ftl b/src/main/webapp/error/403.ftl new file mode 100644 index 00000000..81a64215 --- /dev/null +++ b/src/main/webapp/error/403.ftl @@ -0,0 +1,29 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/macro-common_page.ftl"> + +<@commonPage "403 Forbidden!"> +

    403 Forbidden!

    +403 +
    + ${msg!} + Return to Index. +
    + diff --git a/src/main/webapp/error/404.ftl b/src/main/webapp/error/404.ftl new file mode 100644 index 00000000..80cf80a2 --- /dev/null +++ b/src/main/webapp/error/404.ftl @@ -0,0 +1,28 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/macro-common_page.ftl"> + +<@commonPage "404 Not Found!"> +

    404 Not Found!

    +404 Not Found! +
    + Please start or return to Index. +
    + \ No newline at end of file diff --git a/src/main/webapp/error/500.ftl b/src/main/webapp/error/500.ftl new file mode 100644 index 00000000..ce06e5bf --- /dev/null +++ b/src/main/webapp/error/500.ftl @@ -0,0 +1,28 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> +<#include "../common-template/macro-common_page.ftl"> + +<@commonPage "500 Internal Server Error!"> +

    500 Internal Server Error!

    +500 Internal Server Error! +
    + Please report it or return to Index. +
    + \ No newline at end of file diff --git a/src/main/webapp/images/401.png b/src/main/webapp/images/401.png new file mode 100644 index 00000000..9ba1c2fb Binary files /dev/null and b/src/main/webapp/images/401.png differ diff --git a/src/main/webapp/images/403.png b/src/main/webapp/images/403.png new file mode 100644 index 00000000..5d223501 Binary files /dev/null and b/src/main/webapp/images/403.png differ diff --git a/src/main/webapp/images/404.gif b/src/main/webapp/images/404.gif new file mode 100644 index 00000000..6d8e6155 Binary files /dev/null and b/src/main/webapp/images/404.gif differ diff --git a/src/main/webapp/images/500.png b/src/main/webapp/images/500.png new file mode 100644 index 00000000..47964291 Binary files /dev/null and b/src/main/webapp/images/500.png differ diff --git a/src/main/webapp/images/arrow-left.gif b/src/main/webapp/images/arrow-left.gif new file mode 100644 index 00000000..42853cc9 Binary files /dev/null and b/src/main/webapp/images/arrow-left.gif differ diff --git a/src/main/webapp/images/arrow-left.png b/src/main/webapp/images/arrow-left.png new file mode 100644 index 00000000..0dee8b34 Binary files /dev/null and b/src/main/webapp/images/arrow-left.png differ diff --git a/src/main/webapp/images/arrow-right.gif b/src/main/webapp/images/arrow-right.gif new file mode 100644 index 00000000..39b25bf5 Binary files /dev/null and b/src/main/webapp/images/arrow-right.gif differ diff --git a/src/main/webapp/images/arrow-right.png b/src/main/webapp/images/arrow-right.png new file mode 100644 index 00000000..5e7bd9ed Binary files /dev/null and b/src/main/webapp/images/arrow-right.png differ diff --git a/src/main/webapp/images/arrows.png b/src/main/webapp/images/arrows.png new file mode 100644 index 00000000..775a7a03 Binary files /dev/null and b/src/main/webapp/images/arrows.png differ diff --git a/src/main/webapp/images/bowknot/completed.png b/src/main/webapp/images/bowknot/completed.png new file mode 100644 index 00000000..ea86a168 Binary files /dev/null and b/src/main/webapp/images/bowknot/completed.png differ diff --git a/src/main/webapp/images/bowknot/dialog-close-hover.png b/src/main/webapp/images/bowknot/dialog-close-hover.png new file mode 100644 index 00000000..f850f951 Binary files /dev/null and b/src/main/webapp/images/bowknot/dialog-close-hover.png differ diff --git a/src/main/webapp/images/bowknot/dialog-close.png b/src/main/webapp/images/bowknot/dialog-close.png new file mode 100644 index 00000000..d88a7f84 Binary files /dev/null and b/src/main/webapp/images/bowknot/dialog-close.png differ diff --git a/src/main/webapp/images/default-user-thumbnail.png b/src/main/webapp/images/default-user-thumbnail.png new file mode 100644 index 00000000..e342a74c Binary files /dev/null and b/src/main/webapp/images/default-user-thumbnail.png differ diff --git a/src/main/webapp/images/favicon.png b/src/main/webapp/images/favicon.png new file mode 100644 index 00000000..de7bd6af Binary files /dev/null and b/src/main/webapp/images/favicon.png differ diff --git a/src/main/webapp/images/feed.png b/src/main/webapp/images/feed.png new file mode 100644 index 00000000..b0f72ffe Binary files /dev/null and b/src/main/webapp/images/feed.png differ diff --git a/src/main/webapp/images/github-icon.png b/src/main/webapp/images/github-icon.png new file mode 100644 index 00000000..e6a0323d Binary files /dev/null and b/src/main/webapp/images/github-icon.png differ diff --git a/src/main/webapp/images/github.gif b/src/main/webapp/images/github.gif new file mode 100644 index 00000000..8320dd6b Binary files /dev/null and b/src/main/webapp/images/github.gif differ diff --git a/src/main/webapp/images/github.png b/src/main/webapp/images/github.png new file mode 100644 index 00000000..81f36854 Binary files /dev/null and b/src/main/webapp/images/github.png differ diff --git a/src/main/webapp/images/icon.png b/src/main/webapp/images/icon.png new file mode 100644 index 00000000..5e1b75cf Binary files /dev/null and b/src/main/webapp/images/icon.png differ diff --git a/src/main/webapp/images/kill-browser.png b/src/main/webapp/images/kill-browser.png new file mode 100644 index 00000000..300f8e8d Binary files /dev/null and b/src/main/webapp/images/kill-browser.png differ diff --git a/src/main/webapp/images/loader.gif b/src/main/webapp/images/loader.gif new file mode 100644 index 00000000..6b089de1 Binary files /dev/null and b/src/main/webapp/images/loader.gif differ diff --git a/src/main/webapp/images/loading.gif b/src/main/webapp/images/loading.gif new file mode 100644 index 00000000..9d2d09e0 Binary files /dev/null and b/src/main/webapp/images/loading.gif differ diff --git a/src/main/webapp/images/logo.jpg b/src/main/webapp/images/logo.jpg new file mode 100644 index 00000000..c7c479e2 Binary files /dev/null and b/src/main/webapp/images/logo.jpg differ diff --git a/src/main/webapp/images/logo.png b/src/main/webapp/images/logo.png new file mode 100644 index 00000000..38ac7892 Binary files /dev/null and b/src/main/webapp/images/logo.png differ diff --git a/src/main/webapp/images/logo/logo@144.png b/src/main/webapp/images/logo/logo@144.png new file mode 100644 index 00000000..2fdf21a0 Binary files /dev/null and b/src/main/webapp/images/logo/logo@144.png differ diff --git a/src/main/webapp/images/logo/logo@192.png b/src/main/webapp/images/logo/logo@192.png new file mode 100644 index 00000000..e1495035 Binary files /dev/null and b/src/main/webapp/images/logo/logo@192.png differ diff --git a/src/main/webapp/images/logo/logo@48.png b/src/main/webapp/images/logo/logo@48.png new file mode 100644 index 00000000..b9710ed3 Binary files /dev/null and b/src/main/webapp/images/logo/logo@48.png differ diff --git a/src/main/webapp/images/logo/logo@512.png b/src/main/webapp/images/logo/logo@512.png new file mode 100644 index 00000000..38ac7892 Binary files /dev/null and b/src/main/webapp/images/logo/logo@512.png differ diff --git a/src/main/webapp/images/logo/logo@72.png b/src/main/webapp/images/logo/logo@72.png new file mode 100644 index 00000000..8cca782a Binary files /dev/null and b/src/main/webapp/images/logo/logo@72.png differ diff --git a/src/main/webapp/images/logo/logo@96.png b/src/main/webapp/images/logo/logo@96.png new file mode 100644 index 00000000..c128b7c9 Binary files /dev/null and b/src/main/webapp/images/logo/logo@96.png differ diff --git a/src/main/webapp/images/zz.jpg b/src/main/webapp/images/zz.jpg new file mode 100644 index 00000000..02872c36 Binary files /dev/null and b/src/main/webapp/images/zz.jpg differ diff --git a/src/main/webapp/js/admin/about.js b/src/main/webapp/js/admin/about.js new file mode 100644 index 00000000..ecc66e76 --- /dev/null +++ b/src/main/webapp/js/admin/about.js @@ -0,0 +1,87 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * about for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.1.0.0, Jul 17, 2019 + */ + +/* about 相关操作 */ +admin.about = { + init: function () { + $.ajax({ + url: 'https://rhythm.b3log.org/version/solo/latest/' + Label.version, + type: 'GET', + cache: false, + dataType: 'jsonp', + success: function (data, textStatus) { + var version = data.soloVersion + if (version === Label.version) { + $('#aboutLatest').text(Label.upToDateLabel) + } else { + $('#aboutLatest').html(Label.outOfDateLabel + + '' + version + '') + } + }, + complete: function (XHR, TS) { + admin.clearTip() + }, + }) + + $.ajax({ + url: 'https://hacpai.com/apis/sponsors', + type: 'GET', + dataType: 'jsonp', + jsonp: 'callback', + success: function (data, textStatus) { + var payments = data.data.payments + var sponsprsHTML = '' + for (var i = 0; i < payments.length; i++) { + var userName = '匿名好心人' + if (payments[i].paymentUserName) { + userName = '' + payments[i].paymentUserName + + '' + } + sponsprsHTML += '
  • ' + userName + '  ' + + payments[i].paymentAmount + 'RMB
    ' + + payments[i].paymentMemo + '
  • ' + } + $('#adminAboutSponsors').html(sponsprsHTML) + }, + complete: function (XHR, TS) { + admin.clearTip() + }, + }) + + }, +} + +/* + * 注册到 admin 进行管理 + */ +admin.register['about'] = { + 'obj': admin.about, + 'init': admin.about.init, + 'refresh': function () { + admin.clearTip() + }, +} \ No newline at end of file diff --git a/src/main/webapp/js/admin/admin.js b/src/main/webapp/js/admin/admin.js new file mode 100644 index 00000000..7edcaf35 --- /dev/null +++ b/src/main/webapp/js/admin/admin.js @@ -0,0 +1,283 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @description index for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.4.0.0, Apr 22, 2019 + */ + +Util.htmlDecode = function (code) { + var div = document.createElement('div') + div.innerHTML = decodeURIComponent(code) + return div.innerText +} + +var Admin = function () { + this.register = {} + // 工具栏下的工具 + this.tools = [ + '#page-list', '#theme-list', '#link-list', '#preference', + '#user-list', '#plugin-list', '#others', '#category-list'] + // 多用户时,一般用户不能使用的功能 + this.adTools = [ + 'link-list', 'preference', 'theme-list', 'page-list', + 'user-list', 'plugin-list', 'others', 'category-list'] +} + +$.extend(Admin.prototype, { + /** + * @description 登出 + */ + logout: function () { + window.location.href = Label.servePath + '/logout' + }, + toggleMenu: function () { + if ($('#tabs').css('left') === '-240px') { + $('#tabs').css('left', 0) + $('.tabs__bg').show() + } else { + $('#tabs').css('left', '-240px') + $('.tabs__bg').hide() + } + }, + /** + * @description 清除提示 + */ + clearTip: function () { + $('#tipMsg').text('') + $('#loadMsg').text('') + }, + /** + * @description 根据当前页数设置 hash + * @param {Int} currentPage 当前页 + */ + setHashByPage: function (currentPage) { + var hash = window.location.hash, + hashList = hash.split('/') + if (/^\d*$/.test(hashList[hashList.length - 1])) { + hashList[hashList.length - 1] = currentPage + } else { + hashList.push(currentPage) + } + window.location.hash = hashList.join('/') + }, + /** + * @description 设置某个 tab 被选择 + * @param {Stirng} id id tab id + */ + selectTab: function (id) { + window.location.hash = '#' + id + }, + /** + * @description 根据当前 hash 解析出当前页数及 hash 数组。 + */ + analyseHash: function () { + var hash = window.location.hash + var tag = hash.substr(1, hash.length - 1) + var tagList = tag.split('/') + var tags = {} + tags.page = 1, + tags.hashList = [] + for (var i = 0; i < tagList.length; i++) { + if (i === tagList.length - 1 && (/^\d+$/.test(tagList[i]))) { + tags.page = tagList[i] + } else { + tags.hashList.push(tagList[i]) + } + } + return tags + }, + /** + * @description 根据当前 hash 设置当前 tab + */ + setCurByHash: function () { + $(window).scrollTop(0) + $('#tipMsg').text('') + var tags = admin.analyseHash() + var tab = tags.hashList[1], + subTab = tags.hashList[2] + + if (tags.hashList.length === 1) { + tab = tags.hashList[0] + } + + if (tab === '') { + return + } + + // 离开编辑器时进行提示 + try { + if (admin.editors.articleEditor.getContent) { + // 除更新、发布、取消发布文章,编辑器中无内容外,离开编辑器需进行提示。 + if (tab !== 'article' && admin.article.isConfirm && + admin.editors.articleEditor.getContent().replace(/\s/g, '') !== '' + && admin.article.content !== + admin.editors.articleEditor.getContent()) { + if (!confirm(Label.editorLeaveLabel)) { + window.location.hash = '#article/article' + return + } + } + // 不离开编辑器,hash 需变为 "#article/article",此时不需要做任何处理。 + if (tab === 'article' && admin.article.isConfirm && + admin.editors.articleEditor.getContent().replace(/\s/g, '') !== '' + && admin.article.content !== + admin.editors.articleEditor.getContent()) { + return + } + } + } catch (e) { + console.log(e) + } + + // clear article + if (tab !== 'article' && admin.editors.articleEditor.setContent) { + admin.article.clear() + } + admin.article.isConfirm = true + + $('#tabs').tabs('setCurrent', tab) + $('#loadMsg').text(Label.loadingLabel) + + if ($('#tabsPanel_' + tab).length === 1) { + if ($('#tabsPanel_' + tab).html().replace(/\s/g, '') === '') { + // 还未加载 HTML + $('#tabsPanel_' + tab).load('admin-' + tab + '.do', function () { + // 页面加载完后,回调初始函数 + if (tab === 'article' && admin.article.status.id) { + // 当文章页面编辑器未初始化时,调用更新文章需先初始化编辑器 + admin.register[tab].init.call(admin.register[tab].obj, + admin.article.getAndSet) + } else { + admin.register[tab].init.call(admin.register[tab].obj, tags.page) + } + + // 页面包含子 tab,需根据 hash 定位到相应的 tab + if (subTab) { + $('#tab' + tab.substring(0, 1).toUpperCase() + tab.substring(1)). + tabs('setCurrent', subTab) + } + + // 根据 hash 调用现有的插件函数 + admin.plugin.setCurByHash(tags) + }) + } else { + if (tab === 'article' && admin.article.status.id) { + admin.article.getAndSet() + } + + // 已加载过 HTML,只需调用刷新函数 + if (admin.register[tab] && admin.register[tab].refresh) { + admin.register[tab].refresh.call(admin.register[tab].obj, tags.page) + } + + // 页面包含子 tab,需根据 hash 定位到相应的 tab + if (subTab) { + $('#tab' + tab.substring(0, 1).toUpperCase() + tab.substring(1)). + tabs('setCurrent', subTab) + } + + // 根据 hash 调用现有的插件函数 + admin.plugin.setCurByHash(tags) + } + } else { + $('#tipMsg').text('Error: No tab! ' + Label.reportIssueLabel) + $('#loadMsg').text('') + } + }, + /** + * @description 初始化整个后台 + */ + init: function () { + Util.killIE() + $('#loadMsg').text(Label.loadingLabel) + + // 构建 tabs + $('#tabs').tabs() + + // tipMsg + setInterval(function () { + if ($('#tipMsg').text() !== '') { + setTimeout(function () { + $('#tipMsg').text('') + }, 7000) + } + }, 6000) + $('#loadMsg').text('') + + window.onbeforeunload = function (event) { + if (window.location.hash === '#article/article') { + if (event) { + event.returnValue = Label.editorLeaveLabel + } + return Label.editorLeaveLabel + } + } + + $(document).ajaxError(function (event, xhr, options, exc) { + if (xhr.status !== 200) { + $('#tipMsg').text(xhr.status + ': ' + exc) + } + }) + }, + /** + * @description tools and article collapse + * @param {Bom} it 触发事件对象 + */ + collapseNav: function (it) { + var subNav = $(it).next() + subNav.slideToggle('normal', function () { + if (this.style.display !== 'none') { + $(it). + find('.icon-chevron-down')[0].className = 'icon-chevron-up fn__right' + $(it).addClass('tab-current') + } else { + $(it). + find('.icon-chevron-up')[0].className = 'icon-chevron-down fn__right' + $(it).removeClass('tab-current') + } + + $('#tabs > ul').height('auto') + $('#tabs > ul').height($('#tabs > ul').height() + 80) + }) + }, + /** + * @description 后台及当前页面所需插件初始化完后,对权限进行控制及当前页面属于 tools 时,tools 选项需展开。 + */ + inited: function () { + // Removes functions with the current user role + if (Label.userRole !== 'adminRole') { + for (var i = 0; i < this.adTools.length; i++) { + $('#tabs').tabs('remove', this.adTools[i]) + } + } else { + // 当前 tab 属于 Tools 时,设其展开 + for (var j = 0; j < this.tools.length; j++) { + if ('#' + window.location.hash.split('/')[1] === this.tools[j]) { + $('#tabToolsTitle').click() + break + } + } + } + this.setCurByHash() + }, +}) + +var admin = new Admin() \ No newline at end of file diff --git a/src/main/webapp/js/admin/admin.min.js b/src/main/webapp/js/admin/admin.min.js new file mode 100644 index 00000000..9da74a0e --- /dev/null +++ b/src/main/webapp/js/admin/admin.min.js @@ -0,0 +1,19 @@ +Util.htmlDecode=function(t){var e=document.createElement("div");return e.innerHTML=decodeURIComponent(t),e.innerText};var Admin=function(){this.register={},this.tools=["#page-list","#theme-list","#link-list","#preference","#user-list","#plugin-list","#others","#category-list"],this.adTools=["link-list","preference","theme-list","page-list","user-list","plugin-list","others","category-list"]};$.extend(Admin.prototype,{logout:function(){window.location.href=Label.servePath+"/logout"},toggleMenu:function(){"-240px"===$("#tabs").css("left")?($("#tabs").css("left",0),$(".tabs__bg").show()):($("#tabs").css("left","-240px"),$(".tabs__bg").hide())},clearTip:function(){$("#tipMsg").text(""),$("#loadMsg").text("")},setHashByPage:function(t){var e=window.location.hash.split("/");/^\d*$/.test(e[e.length-1])?e[e.length-1]=t:e.push(t),window.location.hash=e.join("/")},selectTab:function(t){window.location.hash="#"+t},analyseHash:function(){for(var t=window.location.hash,e=t.substr(1,t.length-1).split("/"),i={page:1,hashList:[]},a=0;a ul").height("auto"),$("#tabs > ul").height($("#tabs > ul").height()+80)})},inited:function(){if("adminRole"!==Label.userRole)for(var t=0;t\*\[\]\(\)\$%\{\}@~]/g,"").replace("/\\s/g","")}},height:this.conf.height,counter:102400,resize:{enable:this.conf.resize},lang:Label.localeString}),"function"==typeof this.conf.fun&&this.conf.fun()},getContent:function(){return this.editor.getValue()},setContent:function(e){this.editor.setValue(e)},remove:function(){document.getElementById(this.editor.vditor.id).outerHTML=""}}),admin.editors.articleEditor={},admin.editors.abstractEditor={}; +var TablePaginate=function(a){this.id=a,this.currentPage=1};$.extend(TablePaginate.prototype,{buildTable:function(a,t){var e={colModel:a,noDataTip:Label.noDataLabel};t||(e.expendRow={index:"expendRow"}),$("#"+this.id+"Table").table(e)},initPagination:function(){var a=this.id;$("#"+a+"Pagination").paginate({bind:function(a,t){t?$("#tipMsg").text(t):admin.setHashByPage(a)},currentPage:1,errorMessage:Label.inputErrorLabel,nextPageText:">",previousPageText:"<",goText:Label.gotoLabel,type:"custom",custom:[1],pageCount:1})},initCommentsDialog:function(){var a=this;$("#"+this.id+"Comments").dialog({modal:!0,hideFooter:!0,close:function(){return admin[a.id+"List"].getList(a.currentPage),!0}})},updateTablePagination:function(a,t,e){if((t=parseInt(t))>e.paginationPageCount&&1span").remove("");for(var i="",a=0;a"+t.tags[a].tagTitle+"";$("#tagCheckboxPanel").html(i+'
    '),$("#loadMsg").text("")}}else $("#loadMsg").text("")}}),this.status?this.status.isArticle?($("#unSubmitArticle").show(),$("#saveArticle").hide(),$("#submitArticle").show()):($("#submitArticle").show(),$("#unSubmitArticle").hide(),$("#saveArticle").show()):($("#submitArticle").show(),$("#unSubmitArticle").hide(),$("#saveArticle").show(),$("#postToCommunityPanel").show())},clear:function(){this.status={id:void 0,isArticle:void 0},this.setStatus(),$("#title").val(""),admin.editors.articleEditor.setContent(""),admin.editors.abstractEditor.setContent(""),$("#tag").val(""),$("#tagCheckboxPanel").hide().find("span").removeClass("selected"),$("#permalink").val(""),$("#articleCammentable").prop("checked",!0),$("#postToCommunity").prop("checked",!1),$(".signs button").each(function(t){this.className=0===t?"selected":""}),$("#articleThumbnail").prop("checked")&&$("#articleThumbnail").click()},init:function(t){$(".signs button").click(function(t){$(".signs button").removeClass("selected"),$(this).addClass("selected")}),$("#tipMsg").text(Label.uploadMsg),$.ajax({url:Label.servePath+"/console/tags",type:"GET",cache:!1,success:function(t,e){if($("#tipMsg").text(t.msg),t.sc){if(!(t.tags.length<=0)){for(var i=[],a=0;a",o=a[m].commentContent;"http://"===a[m].commentURL&&(s=""),n+="
    "+s+a[m].commentName+"",a[m].commentOriginalCommentName&&(n+="@"+a[m].commentOriginalCommentName),n+=""+Label.removeLabel+"  "+$.bowknot.getDate(a[m].commentTime)+" 
    "+o+"
    "}""===n&&(n=Label.noCommentLabel),$("#"+c+"Comments").html(n),Util.parseMarkdown(),Util.parseLanguage(),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},del:function(e,a,n){if(confirm(Label.confirmRemoveLabel+Label.commentLabel+"?")){$("#loadMsg").text(Label.loadingLabel);var t="article";"page"===a&&(t="page"),$.ajax({url:Label.servePath+"/console/"+t+"/comment/"+e,type:"DELETE",cache:!1,success:function(e,t){$("#tipMsg").text(e.msg),e.sc&&admin.comment.getList(n,a),$("#loadMsg").text("")}})}}}; +admin.articleList={tablePagination:new TablePaginate("article"),init:function(e){this.tablePagination.buildTable([{text:Label.titleLabel,index:"title",minWidth:110,style:"padding-left: 12px;font-size:14px;"},{text:Label.authorLabel,index:"author",width:150,style:"padding-left: 12px;"},{text:Label.commentLabel,index:"comments",width:80,style:"padding-left: 12px;"},{text:Label.viewLabel,width:60,index:"articleViewCount",style:"padding-left: 12px;"},{text:Label.dateLabel,index:"date",width:90,style:"padding-left: 12px;"}]),this.tablePagination.initPagination(),this.tablePagination.initCommentsDialog(),this.getList(e);var a=this;$("#articleListBtn").click(function(){a.getList(e)}),$("#articleListInput").keypress(function(t){13===t.keyCode&&a.getList(e)})},syncToHacpai:function(t){$.ajax({url:Label.servePath+"/console/article/push2rhy?id="+t,type:"GET",cache:!1,success:function(t,e){$("#tipMsg").text(Label.pushSuccLabel)}})},getList:function(c){var o=this;$("#loadMsg").text(Label.loadingLabel),$.ajax({url:Label.servePath+"/console/articles/status/published/"+c+"/"+Label.PAGE_SIZE+"/"+Label.WINDOW_SIZE+"?k="+$("#articleListInput").val(),type:"GET",cache:!1,success:function(t,e){if($("#tipMsg").text(t.msg),t.sc){for(var a=t.articles,i=[],l=0;l"+a[l].articleTitle+""+a[l].articleTags+"",i[l].date=$.bowknot.getDate(a[l].articleCreateTime),i[l].comments=a[l].articleCommentCount,i[l].articleViewCount=a[l].articleViewCount,i[l].author=a[l].authorName;var n=a[l].articlePutTop?Label.cancelPutTopLabel:Label.putTopLabel;i[l].expendRow=""+Label.updateLabel+" "+Label.removeLabel+" "+Label.pushToHacpaiLabel+" "+n+" "+Label.commentLabel+""}o.tablePagination.updateTablePagination(i,c,t.pagination),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},popTop:function(t,e){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var a=$(t),i="canceltop",l=Label.putTopLabel;a.html()===Label.putTopLabel&&(i="puttop",l=Label.cancelPutTopLabel),$.ajax({url:Label.servePath+"/console/article/"+i+"/"+e,type:"PUT",cache:!1,success:function(t,e){$("#tipMsg").text(t.msg),t.sc&&a.html(l),$("#loadMsg").text("")}})}},admin.register["article-list"]={obj:admin.articleList,init:admin.articleList.init,refresh:admin.articleList.getList}; +admin.draftList={tablePagination:new TablePaginate("draft"),init:function(t){this.tablePagination.buildTable([{text:Label.titleLabel,index:"title",minWidth:110,style:"padding-left: 12px;font-size:14px;"},{text:Label.authorLabel,index:"author",width:150,style:"padding-left: 12px;"},{text:Label.commentLabel,index:"comments",width:80,style:"padding-left: 12px;"},{text:Label.viewLabel,width:60,index:"articleViewCount",style:"padding-left: 12px;"},{text:Label.dateLabel,index:"date",width:90,style:"padding-left: 12px;"}]),this.tablePagination.initPagination(),this.tablePagination.initCommentsDialog(),this.getList(t)},getList:function(n){$("#loadMsg").text(Label.loadingLabel);var d=this;$.ajax({url:Label.servePath+"/console/articles/status/unpublished/"+n+"/"+Label.PAGE_SIZE+"/"+Label.WINDOW_SIZE,type:"GET",cache:!1,success:function(t,e){if($("#tipMsg").text(t.msg),t.sc){for(var a=t.articles,i=[],l=0;l"+a[l].articleTitle+""+a[l].articleTags+"",i[l].expendRow=""+Label.updateLabel+" "+Label.removeLabel+" "+Label.commentLabel+"";d.tablePagination.updateTablePagination(i,n,t.pagination),$("#loadMsg").text("")}else $("#loadMsg").text("")}})}},admin.register["draft-list"]={obj:admin.draftList,init:admin.draftList.init,refresh:admin.draftList.getList}; +admin.pageList={tablePagination:new TablePaginate("page"),pageInfo:{currentCount:1,pageCount:1,currentPage:1},id:"",init:function(e){this.tablePagination.buildTable([{text:"",index:"pageOrder",width:60,style:"padding-left: 12px;font-size:14px;"},{style:"padding-left: 12px;",text:Label.titleLabel,index:"pageTitle",width:300},{style:"padding-left: 12px;",text:Label.permalinkLabel,index:"pagePermalink",minWidth:100},{style:"padding-left: 12px;",text:Label.openMethodLabel,index:"pageTarget",width:120}]),this.tablePagination.initPagination(),this.getList(e)},getList:function(p){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var l=this;$.ajax({url:Label.servePath+"/console/pages/"+p+"/"+Label.PAGE_SIZE+"/"+Label.WINDOW_SIZE,type:"GET",cache:!1,success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){var t=e.pages,i=[];admin.pageList.pageInfo.currentCount=t.length,admin.pageList.pageInfo.pageCount=0===e.pagination.paginationPageCount?1:e.pagination.paginationPageCount;for(var n=0;n
    ':n===t.length-1?i[n].pageOrder='
    ':i[n].pageOrder='
    ';var g="";""!==t[n].pageIcon&&(g=" "),i[n].pageTitle=g+""+t[n].pageTitle+"",i[n].pagePermalink=""+t[n].pagePermalink+"",i[n].pageTarget=t[n].pageOpenTarget,i[n].expendRow=""+Label.viewLabel+" "+Label.updateLabel+" "+Label.removeLabel+""}l.tablePagination.updateTablePagination(i,p,e.pagination),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},get:function(t){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/page/"+t,type:"GET",cache:!1,success:function(e,a){$("#tipMsg").text(e.msg),e.sc&&(admin.pageList.id=t,$("#pageTitle").val(e.page.pageTitle),$("#pagePermalink").val(e.page.pagePermalink),$("#pageTarget").val(e.page.pageOpenTarget),$("#pageIcon").val(e.page.pageIcon)),$("#loadMsg").text("")}})},del:function(e,a){confirm(Label.confirmRemoveLabel+Label.navLabel+'"'+Util.htmlDecode(a)+'"?')&&($("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/page/"+e,type:"DELETE",cache:!1,success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){var t=admin.pageList.pageInfo.currentPage;1===admin.pageList.pageInfo.currentCount&&1!==admin.pageList.pageInfo.pageCount&&admin.pageList.pageInfo.currentPage===admin.pageList.pageInfo.pageCount&&(admin.pageList.pageInfo.pageCount--,t=admin.pageList.pageInfo.pageCount);var i=window.location.hash.split("/");t==i[i.length-1]?admin.pageList.getList(t):admin.setHashByPage(t),$("#loadMsg").text("")}else $("#loadMsg").text("")}}))},add:function(){if(this.validate()){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var e=$("#pagePermalink").val().replace(/(^\s*)|(\s*$)/g,""),a={page:{pageTitle:$("#pageTitle").val(),pagePermalink:e,pageOpenTarget:$("#pageTarget").val(),pageIcon:$("#pageIcon").val()}};$.ajax({url:Label.servePath+"/console/page/",type:"POST",cache:!1,data:JSON.stringify(a),success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){admin.pageList.id="",$("#pagePermalink").val(""),$("#pageTitle").val(""),$("#pageIcon").val(""),$("#pageTarget").val("_self"),admin.pageList.pageInfo.currentCount===Label.PAGE_SIZE&&admin.pageList.pageInfo.currentPage===admin.pageList.pageInfo.pageCount&&admin.pageList.pageInfo.pageCount++;var t=window.location.hash.split("/");admin.pageList.pageInfo.pageCount==t[t.length-1]?admin.pageList.getList(admin.pageList.pageInfo.pageCount):admin.setHashByPage(admin.pageList.pageInfo.pageCount),$("#loadMsg").text("")}else $("#loadMsg").text("")}})}},update:function(){if(this.validate()){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var e=$("#pagePermalink").val().replace(/(^\s*)|(\s*$)/g,""),a={page:{pageTitle:$("#pageTitle").val(),oId:this.id,pagePermalink:e,pageOpenTarget:$("#pageTarget").val(),pageIcon:$("#pageIcon").val()}};$.ajax({url:Label.servePath+"/console/page/",type:"PUT",cache:!1,data:JSON.stringify(a),success:function(e,a){$("#tipMsg").text(e.msg),e.sc&&(admin.pageList.id="",admin.pageList.getList(admin.pageList.pageInfo.currentPage),$("#pageTitle").val(""),$("#pageIcon").val(""),$("#pagePermalink").val(""),$("#pageTarget").val("_self")),$("#loadMsg").text("")}})}},validate:function(){if(""===$("#pageTitle").val().replace(/\s/g,""))$("#tipMsg").text(Label.titleEmptyLabel),$("#pageTitle").focus();else{if(""!==$("#pagePermalink").val().replace(/\s/g,""))return!0;$("#tipMsg").text(Label.linkEmptyLabel)}return!1},submit:function(){""!==this.id?this.update():this.add()},changeOrder:function(e,a,t){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var i={oId:e.toString(),direction:t};$.ajax({url:Label.servePath+"/console/page/order/",type:"PUT",cache:!1,data:JSON.stringify(i),success:function(e,a){$("#tipMsg").text(e.msg),admin.pageList.getList(admin.pageList.pageInfo.currentPage),$("#loadMsg").text("")}})}},admin.register["page-list"]={obj:admin.pageList,init:admin.pageList.init,refresh:admin.pageList.getList}; +admin.others={init:function(){$("#tabOthers").tabs(),$("#loadMsg").text("")},removeUnusedArchives:function(){$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/archive/unused",type:"DELETE",cache:!1,success:function(e,t){$("#tipMsg").text(e.msg)}})},removeUnusedTags:function(){$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/tag/unused",type:"DELETE",cache:!1,success:function(e,t){$("#tipMsg").text(e.msg)}})},exportSQL:function(){$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/export/sql",type:"GET",cache:!1,success:function(e,t){e.sc?$("#tipMsg").text(e.msg):window.location=Label.servePath+"/console/export/sql"}})},exportJSON:function(){$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/export/json",type:"GET",cache:!1,success:function(e,t){e.sc?$("#tipMsg").text(e.msg):window.location=Label.servePath+"/console/export/json"}})},exportHexo:function(){$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/export/hexo",type:"GET",cache:!1,success:function(e,t){e.sc?$("#tipMsg").text(e.msg):window.location=Label.servePath+"/console/export/hexo"}})},getUnusedTags:function(){$.ajax({url:Label.servePath+"/console/tag/unused",type:"GET",cache:!1,success:function(e,t){($("#tipMsg").text(e.msg),e.sc)?e.unusedTags.length:$("#loadMsg").text("")}})}},admin.register.others={obj:admin.others,init:admin.others.init,refresh:function(){admin.clearTip()}}; +admin.linkList={tablePagination:new TablePaginate("link"),pageInfo:{currentCount:1,pageCount:1,currentPage:1},id:"",init:function(i){this.tablePagination.buildTable([{text:"",index:"linkOrder",width:60},{style:"padding-left: 12px;",text:Label.linkTitleLabel,index:"linkTitle",width:230},{style:"padding-left: 12px;",text:Label.urlLabel,index:"linkAddress",minWidth:180},{style:"padding-left: 12px;",text:Label.linkDescriptionLabel,index:"linkDescription",width:360}]),this.tablePagination.initPagination(),this.getList(i),$("#updateLink").dialog({title:$("#updateLink").data("title"),width:700,height:290,modal:!0,hideFooter:!0})},getList:function(l){$("#loadMsg").text(Label.loadingLabel),0===l&&(l=1),this.pageInfo.currentPage=l;var s=this;$.ajax({url:Label.servePath+"/console/links/"+l+"/"+Label.PAGE_SIZE+"/"+Label.WINDOW_SIZE,type:"GET",cache:!1,success:function(i,e){if($("#tipMsg").text(i.msg),i.sc){var n=i.links,t=[];admin.linkList.pageInfo.currentCount=n.length,admin.linkList.pageInfo.pageCount=0===i.pagination.paginationPageCount?1:i.pagination.paginationPageCount;for(var a=0;a ':a===n.length-1?t[a].linkOrder='
    ':t[a].linkOrder='
    ',t[a].linkTitle=n[a].linkTitle,t[a].linkAddress=""+n[a].linkAddress+"",t[a].linkDescription=n[a].linkDescription,t[a].expendRow=""+Label.viewLabel+" "+Label.updateLabel+" "+Label.removeLabel+"";s.tablePagination.updateTablePagination(t,l,i.pagination),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},add:function(){if(this.validate()){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var i={link:{linkTitle:$("#linkTitle").val(),linkAddress:$("#linkAddress").val(),linkDescription:$("#linkDescription").val()}};$.ajax({url:Label.servePath+"/console/link/",type:"POST",cache:!1,data:JSON.stringify(i),success:function(i,e){if($("#tipMsg").text(i.msg),i.sc){$("#linkTitle").val(""),$("#linkAddress").val(""),$("#linkDescription").val(""),admin.linkList.pageInfo.currentCount===Label.PAGE_SIZE&&admin.linkList.pageInfo.currentPage===admin.linkList.pageInfo.pageCount&&admin.linkList.pageInfo.pageCount++;var n=window.location.hash.split("/");admin.linkList.pageInfo.pageCount!==parseInt(n[n.length-1])&&admin.setHashByPage(admin.linkList.pageInfo.pageCount),admin.linkList.getList(admin.linkList.pageInfo.pageCount),$("#loadMsg").text("")}else $("#loadMsg").text("")}})}},get:function(n){$("#loadMsg").text(Label.loadingLabel),$("#updateLink").dialog("open"),$.ajax({url:Label.servePath+"/console/link/"+n,type:"GET",cache:!1,success:function(i,e){$("#tipMsg").text(i.msg),i.sc&&(admin.linkList.id=n,$("#linkTitleUpdate").val(i.link.linkTitle),$("#linkAddressUpdate").val(i.link.linkAddress),$("#linkDescriptionUpdate").val(i.link.linkDescription)),$("#loadMsg").text("")}})},update:function(){if(this.validate("Update")){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var i={link:{linkTitle:$("#linkTitleUpdate").val(),oId:this.id,linkAddress:$("#linkAddressUpdate").val(),linkDescription:$("#linkDescriptionUpdate").val()}};$.ajax({url:Label.servePath+"/console/link/",type:"PUT",cache:!1,data:JSON.stringify(i),success:function(i,e){$("#updateLink").dialog("close"),$("#tipMsg").text(i.msg),i.sc&&admin.linkList.getList(admin.linkList.pageInfo.currentPage),$("#loadMsg").text("")}})}},del:function(i,e){confirm(Label.confirmRemoveLabel+Label.permalinkLabel+'"'+Util.htmlDecode(e)+'"?')&&($("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/link/"+i,type:"DELETE",cache:!1,success:function(i,e){if($("#tipMsg").text(i.msg),i.sc){var n=admin.linkList.pageInfo.currentPage;1===admin.linkList.pageInfo.currentCount&&1!==admin.linkList.pageInfo.pageCount&&admin.linkList.pageInfo.currentPage===admin.linkList.pageInfo.pageCount&&(admin.linkList.pageInfo.pageCount--,n=admin.linkList.pageInfo.pageCount);var t=window.location.hash.split("/");n!==parseInt(t[t.length-1])&&admin.setHashByPage(n),admin.linkList.getList(n),$("#loadMsg").text("")}else $("#loadMsg").text("")}}))},validate:function(i){if(i||(i=""),""===$("#linkTitle"+i).val().replace(/\s/g,""))$("#tipMsg").text(Label.titleEmptyLabel),$("#linkTitle"+i).focus().val("");else if(""===$("#linkAddress"+i).val().replace(/\s/g,""))$("#tipMsg").text(Label.addressEmptyLabel),$("#linkAddress"+i).focus().val("");else{if(/^\w+:\/\//.test($("#linkAddress"+i).val()))return!0;$("#tipMsg").text(Label.addressInvalidLabel),$("#linkAddress"+i).focus().val("")}return!1},changeOrder:function(i,e,n){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var t={oId:i.toString(),direction:n};$.ajax({url:Label.servePath+"/console/link/order/",type:"PUT",cache:!1,data:JSON.stringify(t),success:function(i,e){$("#tipMsg").text(i.msg),admin.linkList.getList(admin.linkList.pageInfo.currentPage),$("#loadMsg").text("")}})}},admin.register["link-list"]={obj:admin.linkList,init:admin.linkList.init,refresh:admin.linkList.getList}; +admin.preference={locale:"",init:function(){$("#tabPreference").tabs(),$.ajax({url:Label.servePath+"/console/preference/",type:"GET",cache:!1,success:function(result,textStatus){if($("#tipMsg").text(result.msg),result.sc){var preference=result.preference;$("#metaKeywords").val(preference.metaKeywords),$("#metaDescription").val(preference.metaDescription),$("#blogTitle").val(preference.blogTitle),$("#blogSubtitle").val(preference.blogSubtitle),$("#mostCommentArticleDisplayCount").val(preference.mostCommentArticleDisplayCount),$("#mostViewArticleDisplayCount").val(preference.mostViewArticleDisplayCount),$("#recentCommentDisplayCount").val(preference.recentCommentDisplayCount),$("#mostUsedTagDisplayCount").val(preference.mostUsedTagDisplayCount),$("#articleListDisplayCount").val(preference.articleListDisplayCount),$("#articleListPaginationWindowSize").val(preference.articleListPaginationWindowSize),$("#localeString").val(preference.localeString),$("#timeZoneId").val(preference.timeZoneId),$("#noticeBoard").val(preference.noticeBoard),$("#footerContent").val(preference.footerContent),$("#htmlHead").val(preference.htmlHead),$("#externalRelevantArticlesDisplayCount").val(preference.externalRelevantArticlesDisplayCount),$("#relevantArticlesDisplayCount").val(preference.relevantArticlesDisplayCount),$("#randomArticlesDisplayCount").val(preference.randomArticlesDisplayCount),$("#customVars").val(preference.customVars),"true"===preference.enableArticleUpdateHint?$("#enableArticleUpdateHint").attr("checked","checked"):$("#enableArticleUpdateHint").removeAttr("checked"),"true"===preference.allowVisitDraftViaPermalink?$("#allowVisitDraftViaPermalink").attr("checked","checked"):$("allowVisitDraftViaPermalink").removeAttr("checked"),"true"===preference.commentable?$("#commentable").attr("checked","checked"):$("commentable").removeAttr("checked"),"true"===preference.syncGitHub?$("#syncGitHub").attr("checked","checked"):$("syncGitHub").removeAttr("checked"),"true"===preference.pullGitHub?$("#pullGitHub").attr("checked","checked"):$("pullGitHub").removeAttr("checked"),admin.preference.locale=preference.localeString;for(var signs=eval("("+preference.signs+")"),j=1;j
    '+a[t].skinDirName+'
    ',a[t].skinDirName!==i.skin.skinDirName&&(s+='"),a[t].skinDirName!==i.skin.mobileSkinDirName&&(s+='"),s+='
    "}$("#skinMain").html(s+"
    "),$(".skinItem .update").click(function(){admin.themeList.update($(this).data("name"),"pc")}),$(".skinItem .mobile").click(function(){admin.themeList.update($(this).data("name"),"mobile")}),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},update:function(i,e){$("#tipMsg").text(""),$("#loadMsg").text(Label.loadingLabel);var a={skin:{skinDirName:admin.themeList.skinDirName,mobileSkinDirName:admin.themeList.mobileSkinDirName}};"pc"===e?a.skin.skinDirName=i:a.skin.mobileSkinDirName=i,$.ajax({url:Label.servePath+"/console/skin",type:"PUT",cache:!1,data:JSON.stringify(a),success:function(i,e){$("#tipMsg").text(i.msg),i.sc&&admin.themeList.init(),$("#loadMsg").text("")}})}},admin.register["theme-list"]={obj:admin.themeList,init:admin.themeList.init,refresh:function(){$("#loadMsg").text("")}}; +admin.pluginList={tablePagination:new TablePaginate("plugin"),pageInfo:{currentCount:1,pageCount:1,currentPage:1},init:function(t){this.tablePagination.buildTable([{style:"padding-left: 12px;",text:Label.pluginNameLabel,index:"name",width:230},{style:"padding-left: 12px;",text:Label.statusLabel,index:"status",minWidth:80},{style:"padding-left: 12px;",text:Label.authorLabel,index:"author",width:200},{style:"padding-left: 12px;",text:Label.versionLabel,index:"version",width:120}]),this.tablePagination.initPagination(),$("#pluginSetting").dialog({width:700,height:180,modal:!0,hideFooter:!0}),this.getList(t)},getList:function(n){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var l=this;$.ajax({url:Label.servePath+"/console/plugins/"+n+"/"+Label.PAGE_SIZE+"/"+Label.WINDOW_SIZE,type:"GET",cache:!1,success:function(t,e){if($("#tipMsg").text(t.msg),t.sc){admin.pluginList.pageInfo.currentPage=n;for(var a=t.plugins,i=0;i","ENABLED"===a[i].status?(a[i].status=Label.enabledLabel,a[i].expendRow+=Label.disableLabel):(a[i].status=Label.disabledLabel,a[i].expendRow+=Label.enableLabel),a[i].expendRow+=" ","{}"!=a[i].setting&&(a[i].expendRow+=" "+Label.settingLabel+" ");l.tablePagination.updateTablePagination(t.plugins,n,t.pagination),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},toSetting:function(t){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var e={oId:t};$.ajax({url:Label.servePath+"/console/plugin/toSetting",type:"POST",cache:!1,data:JSON.stringify(e),success:function(t,e){$("#tipMsg").text(t.msg),$("#pluginSetting").html(t),$("#pluginSetting").dialog("open"),$("#loadMsg").text("")}})},changeStatus:function(t,e){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var a={oId:t,status:e="ENABLED"===e?"DISABLED":"ENABLED"};$.ajax({url:Label.servePath+"/console/plugin/status/",type:"PUT",cache:!1,data:JSON.stringify(a),success:function(t,e){$("#tipMsg").text(t.msg),t.sc?($("#loadMsg").text(""),window.location.reload()):$("#loadMsg").text("")}})}},admin.register["plugin-list"]={obj:admin.pluginList,init:admin.pluginList.init,refresh:function(){$("#loadMsg").text("")}}; +admin.userList={tablePagination:new TablePaginate("user"),pageInfo:{currentCount:1,pageCount:1,currentPage:1},userInfo:{oId:"",userRole:""},init:function(e){this.tablePagination.buildTable([{style:"padding-left: 12px;",text:Label.userNameLabel,index:"userName",width:230},{style:"padding-left: 12px;",text:Label.roleLabel,index:"isAdmin",width:120}]),this.tablePagination.initPagination(),this.getList(e),$("#userUpdate").dialog({width:700,height:450,modal:!0,hideFooter:!0})},getList:function(n){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),this.pageInfo.currentPage=n;var r=this;$.ajax({url:Label.servePath+"/console/users/"+n+"/"+Label.PAGE_SIZE+"/"+Label.WINDOW_SIZE,type:"GET",cache:!1,success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){var t=e.users,s=[];if(admin.userList.pageInfo.currentCount=t.length,admin.userList.pageInfo.pageCount=e.pagination.paginationPageCount,t.length<1)return $("#tipMsg").text("No user "+Label.reportIssueLabel),void $("#loadMsg").text("");$("#tipMsg").text(Label.uploadMsg);for(var i=0;i"+Label.updateLabel+""):(s[i].expendRow=""+Label.updateLabel+" "+Label.removeLabel+" "+Label.changeRoleLabel+"","defaultRole"===t[i].userRole?s[i].isAdmin=Label.commonUserLabel:s[i].isAdmin=Label.visitorUserLabel),r.tablePagination.updateTablePagination(s,n,e.pagination);$("#loadMsg").text("")}else $("#loadMsg").text("")}})},get:function(t,s){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$("#userUpdate").dialog("open"),$.ajax({url:Label.servePath+"/console/user/"+t,type:"GET",cache:!1,success:function(e,a){$("#tipMsg").text(e.msg),e.sc&&($("#userURLUpdate").val(e.user.userURL),$("#userAvatarUpdate").val(e.user.userAvatar),$("#userB3KeyUpdate").val(e.user.userB3Key),$("#userNameUpdate").val(e.user.userName).data("userInfo",{oId:t,userRole:s})),$("#loadMsg").text("")}})},update:function(){if(this.validate("Update")){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var e=$("#userNameUpdate").data("userInfo"),a={userName:$("#userNameUpdate").val(),oId:e.oId,userURL:$("#userURLUpdate").val(),userRole:e.userRole,userAvatar:$("#userAvatarUpdate").val(),userB3Key:$("#userB3KeyUpdate").val()};$.ajax({url:Label.servePath+"/console/user/",type:"PUT",cache:!1,data:JSON.stringify(a),success:function(e,a){$("#userUpdate").dialog("close"),$("#tipMsg").text(e.msg),e.sc&&admin.userList.getList(admin.userList.pageInfo.currentPage),$("#loadMsg").text("")}})}},del:function(e,a){confirm(Label.confirmRemoveLabel+Label.userLabel+'"'+Util.htmlDecode(a)+'"?')&&($("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/user/"+e,type:"DELETE",cache:!1,success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){var t=admin.userList.pageInfo.currentPage;1===admin.userList.pageInfo.currentCount&&1!==admin.userList.pageInfo.pageCount&&admin.userList.pageInfo.currentPage===admin.userList.pageInfo.pageCount&&(admin.userList.pageInfo.pageCount--,t=admin.userList.pageInfo.pageCount);var s=window.location.hash.split("/");t!==parseInt(s[s.length-1])&&admin.setHashByPage(t),admin.userList.getList(t),$("#loadMsg").text("")}else $("#loadMsg").text("")}}))},changeRole:function(e){$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/changeRole/"+e,type:"GET",cache:!1,success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){var t=admin.userList.pageInfo.currentPage;1===admin.userList.pageInfo.currentCount&&1!==admin.userList.pageInfo.pageCount&&admin.userList.pageInfo.currentPage===admin.userList.pageInfo.pageCount&&(admin.userList.pageInfo.pageCount--,t=admin.userList.pageInfo.pageCount);var s=window.location.hash.split("/");t!==parseInt(s[s.length-1])&&admin.setHashByPage(t),admin.userList.getList(t),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},validate:function(e){e||(e="");var a=$("#userName"+e).val().replace(/(^\s*)|(\s*$)/g,"");return!(a.length<2||20 ':i===t.length-1?o[i].linkOrder='
    ':o[i].linkOrder='
    ',o[i].categoryTitle=t[i].categoryTitle,o[i].categoryURI=t[i].categoryURI,o[i].categoryDesc=t[i].categoryDescription,o[i].expendRow=""+Label.updateLabel+" "+Label.removeLabel+" ";g.tablePagination.updateTablePagination(o,n,e.pagination),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},add:function(){if(this.validate()){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text("");var e={categoryTitle:$("#categoryName").val(),categoryTags:$("#categoryTags").val(),categoryURI:$("#categoryURI").val(),categoryDescription:$("#categoryDesc").val()},o=$("#categoryName").data("oId"),a="POST";o&&(e.oId=o,a="PUT"),$.ajax({url:Label.servePath+"/console/category/",type:a,cache:!1,data:JSON.stringify(e),success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){if(!o){admin.categoryList.pageInfo.currentCount===Label.PAGE_SIZE&&admin.categoryList.pageInfo.currentPage===admin.categoryList.pageInfo.pageCount&&admin.categoryList.pageInfo.pageCount++;var t=window.location.hash.split("/");admin.categoryList.pageInfo.pageCount!==parseInt(t[t.length-1])&&admin.setHashByPage(admin.categoryList.pageInfo.pageCount)}$("#categoryName").val("").data("oId",""),$("#categoryTags").val(""),$("#categoryURI").val(""),$("#categoryDesc").val(""),admin.categoryList.getList(admin.categoryList.pageInfo.pageCount),$("#loadMsg").text("")}else $("#loadMsg").text("")}})}},get:function(t){$("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/category/"+t,type:"GET",cache:!1,success:function(e,a){$("#tipMsg").text(e.msg),e.sc&&($("#categoryName").val(e.categoryTitle).data("oId",t),$("#categoryURI").val(e.categoryURI),$("#categoryDesc").val(e.categoryDescription),$("#categoryTags").val(e.categoryTags)),$("#loadMsg").text("")}})},del:function(e,a){confirm(Label.confirmRemoveLabel+Label.categoryLabel+'"'+Util.htmlDecode(a)+'"?')&&($("#loadMsg").text(Label.loadingLabel),$("#tipMsg").text(""),$.ajax({url:Label.servePath+"/console/category/"+e,type:"DELETE",cache:!1,success:function(e,a){if($("#tipMsg").text(e.msg),e.sc){var t=admin.categoryList.pageInfo.currentPage;1===admin.categoryList.pageInfo.currentCount&&1!==admin.categoryList.pageInfo.pageCount&&admin.categoryList.pageInfo.currentPage===admin.categoryList.pageInfo.pageCount&&(admin.categoryList.pageInfo.pageCount--,t=admin.categoryList.pageInfo.pageCount);var o=window.location.hash.split("/");t!==parseInt(o[o.length-1])&&admin.setHashByPage(t),admin.categoryList.getList(t),$("#loadMsg").text("")}else $("#loadMsg").text("")}}))},validate:function(e){e||(e="");var a=$("#categoryName"+e).val().replace(/(^\s*)|(\s*$)/g,"");if(a.length<2||32'+a[i].commentContent+" on   "+a[i].commentTitle+"",n[i].expendRow=""+Label.removeLabel+"",n[i].title="","http://"===a[i].commentURL?n[i].title+=a[i].commentName:n[i].title+=""+a[i].commentName+"",n[i].title+="
    ",n[i].date=$.bowknot.getDate(a[i].commentTime)}o.tablePagination.updateTablePagination(n,l,e.pagination),Util.parseMarkdown(),Util.parseLanguage(),$("#loadMsg").text("")}else $("#loadMsg").text("")}})},del:function(e,t){confirm(Label.confirmRemoveLabel+Label.commentLabel+"?")&&($("#loadMsg").text(Label.loadingLabel),$.ajax({url:Label.servePath+"/console/"+t.toLowerCase()+"/comment/"+e,type:"DELETE",cache:!1,success:function(e,t){$("#tipMsg").text(e.msg),e.sc&&admin.commentList.getList(admin.commentList.pageInfo.currentPage),$("#loadMsg").text("")}}))}},admin.register["comment-list"]={obj:admin.commentList,init:admin.commentList.init,refresh:admin.commentList.getList}; +var plugins={};admin.plugin={plugins:[],add:function(t){t.isInit=!1,t.hash=t.path.replace("/","#")+"/"+t.id,this.plugins.push(t);var a=this._analysePath(t.path);t.index&&a.length<2&&this._addNew(t,a)},setCurByHash:function(t){for(var a=this.plugins,e=0;eul>li").get(t.index-1):"article"===a[0]?t.target=$("#tabArticleMgt>li").get(t.index-1):"tools"===a[0]&&(admin.tools.push("#"+t.id),t.target=$("#tabTools>li").get(t.index-1)),t.target||alert("data.index is error!"),$("#tabs").tabs("add",t)},_addToExist:function(t,a){switch(a[0]){case"main":$("#mainPanel"+a[1].charAt(5)).append(t.content);break;case"tools":case"article":2===a.length?$("#tabsPanel_"+a[1]).append(t.content):$("#tabPreferencePanel_"+a[2]).append(t.content);break;case"comment-list":$("#tabsPanel_comment-list").append(t.content)}}}; +admin.main={},admin.register.main={obj:admin.main,init:function(){admin.clearTip()},refresh:function(){admin.clearTip()}}; +admin.about={init:function(){$.ajax({url:"https://rhythm.b3log.org/version/solo/latest/"+Label.version,type:"GET",cache:!1,dataType:"jsonp",success:function(a,e){var t=a.soloVersion;t===Label.version?$("#aboutLatest").text(Label.upToDateLabel):$("#aboutLatest").html(Label.outOfDateLabel+""+t+"")},complete:function(a,e){admin.clearTip()}}),$.ajax({url:"https://hacpai.com/apis/sponsors",type:"GET",dataType:"jsonp",jsonp:"callback",success:function(a,e){for(var t=a.data.payments,n="",s=0;s'+t[s].paymentUserName+""),n+='
  • '+o+'  '+t[s].paymentAmount+'RMB
    "+t[s].paymentMemo+"
  • "}$("#adminAboutSponsors").html(n)},complete:function(a,e){admin.clearTip()}})}},admin.register.about={obj:admin.about,init:admin.about.init,refresh:function(){admin.clearTip()}}; \ No newline at end of file diff --git a/src/main/webapp/js/admin/article.js b/src/main/webapp/js/admin/article.js new file mode 100644 index 00000000..b1cf4ec2 --- /dev/null +++ b/src/main/webapp/js/admin/article.js @@ -0,0 +1,562 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @fileoverview article for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.6.0.5, Aug 6, 2019 + */ +admin.article = { + // 当发文章,取消发布,更新文章时设置为 false。不需在离开编辑器时进行提示。 + isConfirm: true, + status: { + id: undefined, + isArticle: undefined, + }, + content: '', + /** + * @description 获取文章并把值塞入发布文章页面 + * @param {String} id 文章 id + * @param {Boolean} isArticle 文章或者草稿 + */ + get: function (id, isArticle) { + this.status.id = id + this.status.isArticle = isArticle + admin.selectTab('article/article') + }, + /** + * @description 获取文章内容 + */ + getAndSet: function () { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + $.ajax({ + url: Label.servePath + '/console/article/' + + admin.article.status.id, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + // set default value for article. + $('#title').val(result.article.articleTitle) + admin.editors.articleEditor.setContent(result.article.articleContent) + admin.editors.abstractEditor.setContent(result.article.articleAbstract) + admin.article.content = admin.editors.articleEditor.getContent() + + var tags = result.article.articleTags, + tagsString = '' + for (var i = 0; i < tags.length; i++) { + if (0 === i) { + tagsString = tags[i].tagTitle + } else { + tagsString += ',' + tags[i].tagTitle + } + } + + $('#tag').val(tagsString) + $('#permalink').val(result.article.articlePermalink) + $('#viewPwd').val(result.article.articleViewPwd) + + $('#articleCommentable'). + prop('checked', result.article.articleCommentable) + + // signs + var signs = result.article.signs + $('.signs button').each(function (i) { + if (parseInt(result.article.articleSignId) === + parseInt(signs[i].oId)) { + $('#articleSign' + signs[i].oId).addClass('selected') + } else { + $('#articleSign' + signs[i].oId).removeClass('selected') + } + }) + + admin.article.setStatus() + $('#loadMsg').text('') + }, + }) + }, + /** + * @description 删除文章 + * @param {String} id 文章 id + * @param {String} fromId 文章来自草稿夹(draft)/文件夹(article) + * @param {String} title 文章标题 + */ + del: function (id, fromId, title) { + var isDelete = confirm(Label.confirmRemoveLabel + Label.articleLabel + '"' + + Util.htmlDecode(title) + '"?') + if (isDelete) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + + $.ajax({ + url: Label.servePath + '/console/article/' + id, + type: 'DELETE', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + admin[fromId + 'List'].getList(1) + }, + }) + } + }, + /** + * @@description 添加文章 + * @param {Boolean} articleStatus 0:已发布,1:草稿 + */ + add: function (articleStatus) { + if (admin.article.validate()) { + var that = this + that._addDisabled() + + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + var signId = '' + $('.signs button').each(function () { + if (this.className === 'selected') { + signId = this.id.substr(this.id.length - 1, 1) + } + }) + + var articleContent = admin.editors.articleEditor.getContent(), + articleAbstract = admin.editors.abstractEditor.getContent() + + if ($('#articleThumbnail').prop('checked')) { + var bgImage = $('.thumbnail__img').css('background-image') + articleContent = '![](' + bgImage.substring(5, bgImage.length - 2). + replace('w/768', 'w/960'). + replace('h/432', 'h/540') + + ')\n\n' + articleContent + } + + var requestJSONObject = { + 'article': { + 'articleTitle': $('#title').val(), + 'articleContent': articleContent, + 'articleAbstract': articleAbstract, + 'articleTags': this.trimUniqueArray($('#tag').val()).toString(), + 'articlePermalink': $('#permalink').val(), + 'articleStatus': articleStatus, + 'articleSignId': signId, + 'postToCommunity': $('#postToCommunity').prop('checked'), + 'articleCommentable': $('#articleCommentable').prop('checked'), + 'articleViewPwd': $('#viewPwd').val(), + }, + } + + $.ajax({ + url: Label.servePath + '/console/article/', + type: 'POST', + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + return + } + + admin.article.status.id = undefined + if (articleStatus === 0) { + admin.selectTab('article/article-list') + } else { + admin.selectTab('article/draft-list') + } + + admin.article.isConfirm = false + }, + complete: function (jqXHR, textStatus) { + that._removeDisabled() + $('#loadMsg').text('') + }, + }) + } + }, + /** + * @description 更新文章 + * @param {Boolean} articleStatus 0:已发布,1:草稿 + */ + update: function (articleStatus) { + if (admin.article.validate()) { + var that = this + that._addDisabled() + + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + var signId = '' + $('.signs button').each(function () { + if (this.className === 'selected') { + signId = this.id.substr(this.id.length - 1, 1) + } + }) + + var articleContent = admin.editors.articleEditor.getContent(), + articleAbstract = admin.editors.abstractEditor.getContent() + if ($('#articleThumbnail').prop('checked')) { + var bgImage = $('.thumbnail__img').css('background-image') + articleContent = '![](' + bgImage.substring(5, bgImage.length - 2). + replace('w/768', 'w/960'). + replace('h/432', 'h/540') + + ') \n\n' + articleContent + } + var requestJSONObject = { + 'article': { + 'oId': this.status.id, + 'articleTitle': $('#title').val(), + 'articleContent': articleContent, + 'articleAbstract': articleAbstract, + 'articleTags': this.trimUniqueArray($('#tag').val()).toString(), + 'articlePermalink': $('#permalink').val(), + 'articleStatus': articleStatus, + 'articleSignId': signId, + 'articleCommentable': $('#articleCommentable').prop('checked'), + 'articleViewPwd': $('#viewPwd').val(), + 'postToCommunity': $('#postToCommunity').prop('checked'), + }, + } + + $.ajax({ + url: Label.servePath + '/console/article/', + type: 'PUT', + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + return + } + + if (articleStatus === 0) { + admin.selectTab('article/article-list') + } else { + admin.selectTab('article/draft-list') + } + + $('#tipMsg').text(Label.updateSuccLabel) + + admin.article.status.id = undefined + admin.article.isConfirm = false + }, + complete: function (jqXHR, textStatus) { + that._removeDisabled() + $('#loadMsg').text('') + }, + }) + } + }, + /** + * @description 发布文章页面设置文章按钮、发布到社区等状态的显示 + */ + setStatus: function () { + $.ajax({// Gets all tags + url: Label.servePath + '/console/tags', + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + if (0 >= result.tags.length) { + return + } + + $('#tagCheckboxPanel>span').remove('') + + var spans = '' + for (var i = 0; i < result.tags.length; i++) { + spans += '' + result.tags[i].tagTitle + '' + } + $('#tagCheckboxPanel').html(spans + '
    ') + + $('#loadMsg').text('') + }, + }) + + // set button status + if (this.status) { + if (this.status.isArticle) { + $('#unSubmitArticle').show() + $('#saveArticle').hide() + $('#submitArticle').show() + } else { + $('#submitArticle').show() + $('#unSubmitArticle').hide() + $('#saveArticle').show() + } + } else { + $('#submitArticle').show() + $('#unSubmitArticle').hide() + $('#saveArticle').show() + $('#postToCommunityPanel').show() + } + }, + /** + * @description 清除发布文章页面的输入框的内容 + */ + clear: function () { + this.status = { + id: undefined, + isArticle: undefined, + } + this.setStatus() + + $('#title').val('') + + admin.editors.articleEditor.setContent('') + admin.editors.abstractEditor.setContent('') + + // reset tag + $('#tag').val('') + $('#tagCheckboxPanel').hide().find('span').removeClass('selected') + + $('#permalink').val('') + $('#articleCammentable').prop('checked', true) + $('#postToCommunity').prop('checked', false) + $('.signs button').each(function (i) { + if (i === 0) { + this.className = 'selected' + } else { + this.className = '' + } + }) + + if ($('#articleThumbnail').prop('checked')) { + $('#articleThumbnail').click() + } + }, + /** + * @description 初始化发布文章页面 + */ + init: function (fun) { + // Inits Signs. + $('.signs button').click(function (i) { + $('.signs button').removeClass('selected') + $(this).addClass('selected') + }) + + $('#tipMsg').text(Label.uploadMsg) + + // For tag auto-completion + $.ajax({// Gets all tags + url: Label.servePath + '/console/tags', + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + if (0 >= result.tags.length) { + return + } + + var tags = [] + for (var i = 0; i < result.tags.length; i++) { + tags.push(result.tags[i].tagTitle) + } + + $('#tag').completed({ + height: 160, + buttonText: Label.selectLabel, + data: tags, + }) + + $('#loadMsg').text('') + }, + }) + + // submit action + $('#submitArticle').click(function () { + if (admin.article.status.id) { + admin.article.update(0) + } else { + admin.article.add(0) + } + }) + + $('#saveArticle').click(function () { + if (admin.article.status.id) { + admin.article.update(admin.article.status.isArticle ? 0 : 1) + } else { + admin.article.add(1) + } + }) + + // editor + admin.editors.articleEditor = new SoloEditor({ + id: 'articleContent', + height: 500, + fun: fun, + previewMode: 'both', + resize: false, + }) + + admin.editors.abstractEditor = new SoloEditor({ + id: 'abstract', + height: 200, + previewMode: 'editor', + resize: true, + }) + + // thumbnail + $('#articleThumbnailBtn').click(function () { + $.ajax({// Gets all tags + url: Label.servePath + '/console/thumbs?n=1&w=768&h=432', + type: 'GET', + cache: false, + success: function (result, textStatus) { + if (!result.sc) { + $('#loadMsg').text(result.msg) + return + } + + $('#articleThumbnailBtn'). + prev(). + css('background-image', 'url(' + result.data[0] + ')') + }, + }) + }).click() + }, + /** + * @description 验证发布文章字段的合法性 + */ + validate: function () { + var articleContent = admin.editors.articleEditor.getContent() + + if ($('#title').val().replace(/\s/g, '') === '') { + $('#tipMsg').text(Label.titleEmptyLabel) + $('#title').focus().val('') + } else if (articleContent.replace(/\s/g, '') === '') { + $('#tipMsg').text(Label.contentEmptyLabel) + } else { + return true + } + return false + }, + /** + * @description 取消发布 + */ + unPublish: function () { + var that = this + that._addDisabled() + $.ajax({ + url: Label.servePath + '/console/article/unpublish/' + + admin.article.status.id, + type: 'PUT', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + return + } + + admin.selectTab('article/draft-list') + admin.article.status.id = undefined + admin.article.isConfirm = false + }, + complete: function (jqXHR, textStatus) { + that._removeDisabled() + $('#loadMsg').text('') + }, + }) + }, + /** + * @description 数组中无重复 + * @param {String} str 被解析的字符串 + * @returns {String} 无重复的字符串 + */ + trimUniqueArray: function (str) { + str = str.toString() + var arr = str.split(',') + for (var i = 0; i < arr.length; i++) { + arr[i] = arr[i].replace(/(^\s*)|(\s*$)/g, '') + if (arr[i] === '') { + arr.splice(i, 1) + i-- + } + } + var unique = $.unique(arr) + return unique.toString() + }, + /** + * @description 点击发文文章时的处理 + */ + prePost: function () { + $('#loadMsg').text(Label.loadingLabel) + admin.article.content = '' + if (!admin.editors.articleEditor.getContent) { + return + } + + var articleContent = admin.editors.articleEditor.getContent() + + if (window.location.hash === '#article/article' && + articleContent.replace(/\s/g, '') !== '') { + if (confirm(Label.editorPostLabel)) { + admin.article.clear() + } + } + $('#tipMsg').text('') + $('#loadMsg').text('') + }, + /** + * @description: 仿重复提交,点击一次后,按钮设置为 disabled + */ + _addDisabled: function () { + $('#unSubmitArticle').attr('disabled', 'disabled') + $('#saveArticle').attr('disabled', 'disabled') + $('#submitArticle').attr('disabled', 'disabled') + }, + /** + * @description: 仿重复提交,当后台有数据返回后,按钮移除 disabled 状态 + */ + _removeDisabled: function () { + $('#unSubmitArticle').removeAttr('disabled') + $('#saveArticle').removeAttr('disabled') + $('#submitArticle').removeAttr('disabled') + }, +} + +/** + * @description 注册到 admin 进行管理 + */ +admin.register.article = { + 'obj': admin.article, + 'init': admin.article.init, + 'refresh': function () { + admin.editors.abstractEditor.setContent('') + admin.editors.articleEditor.setContent('') + $('#loadMsg').text('') + $('#tipMsg').text(Label.uploadMsg) + }, +} diff --git a/src/main/webapp/js/admin/articleList.js b/src/main/webapp/js/admin/articleList.js new file mode 100644 index 00000000..185ef7a2 --- /dev/null +++ b/src/main/webapp/js/admin/articleList.js @@ -0,0 +1,195 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * article list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.3.0.0, Sep 14, 2019 + */ + +/* article-list 相关操作 */ +admin.articleList = { + tablePagination: new TablePaginate('article'), + + /* + * 初始化 table, pagination, comments dialog + */ + init: function (page) { + this.tablePagination.buildTable([ + { + text: Label.titleLabel, + index: 'title', + minWidth: 110, + style: 'padding-left: 12px;font-size:14px;', + }, { + text: Label.authorLabel, + index: 'author', + width: 150, + style: 'padding-left: 12px;', + }, { + text: Label.commentLabel, + index: 'comments', + width: 80, + style: 'padding-left: 12px;', + }, { + text: Label.viewLabel, + width: 60, + index: 'articleViewCount', + style: 'padding-left: 12px;', + }, { + text: Label.dateLabel, + index: 'date', + width: 90, + style: 'padding-left: 12px;', + }]) + this.tablePagination.initPagination() + this.tablePagination.initCommentsDialog() + this.getList(page) + + var that = this + $('#articleListBtn').click(function () { + that.getList(page) + }) + $('#articleListInput').keypress(function(event) { + if (event.keyCode === 13) { + that.getList(page) + } + }) + }, + + /** + * 同步到黑客派社区 + * @param id 文章 id + */ + syncToHacpai: function (id) { + $.ajax({ + url: Label.servePath + '/console/article/push2rhy?id=' + id, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(Label.pushSuccLabel) + }, + }) + }, + + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function (pageNum) { + var that = this + $('#loadMsg').text(Label.loadingLabel) + $.ajax({ + url: Label.servePath + '/console/articles/status/published/' + + pageNum + '/' + Label.PAGE_SIZE + '/' + Label.WINDOW_SIZE + '?k=' + + $('#articleListInput').val(), + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + var articles = result.articles, + articleData = [] + for (var i = 0; i < articles.length; i++) { + articleData[i] = {} + articleData[i].title = '' + + articles[i].articleTitle + '' + + articles[i].articleTags + '' + articleData[i].date = $.bowknot.getDate(articles[i].articleCreateTime) + articleData[i].comments = articles[i].articleCommentCount + articleData[i].articleViewCount = articles[i].articleViewCount + articleData[i].author = articles[i].authorName + + var topClass = articles[i].articlePutTop + ? Label.cancelPutTopLabel + : Label.putTopLabel + articleData[i].expendRow = '' + Label.updateLabel + ' \ + ' + + Label.removeLabel + ' \ + ' + Label.pushToHacpaiLabel + ' \ + ' + topClass + ' \ + ' + Label.commentLabel + '' + } + + that.tablePagination.updateTablePagination(articleData, pageNum, + result.pagination) + + $('#loadMsg').text('') + }, + }) + } + + , + + /* + * 制定或者取消置顶 + * @it 触发事件的元素本身 + * @id 草稿 id + */ + popTop: function (it, id) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + + var $it = $(it), + ajaxUrl = 'canceltop', + tip = Label.putTopLabel + + if ($it.html() === Label.putTopLabel) { + ajaxUrl = 'puttop' + tip = Label.cancelPutTopLabel + } + + $.ajax({ + url: Label.servePath + '/console/article/' + ajaxUrl + '/' + id, + type: 'PUT', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + $it.html(tip) + $('#loadMsg').text('') + }, + }) + }, +} + +/* + * 注册到 admin 进行管理 + */ +admin.register['article-list'] = { + 'obj': admin.articleList, + 'init': admin.articleList.init, + 'refresh': admin.articleList.getList, +} diff --git a/src/main/webapp/js/admin/categoryList.js b/src/main/webapp/js/admin/categoryList.js new file mode 100644 index 00000000..916e5998 --- /dev/null +++ b/src/main/webapp/js/admin/categoryList.js @@ -0,0 +1,336 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * category list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.2.0.0, Apr 28, 2019 + * @since 2.0.0 + */ + +/* category-list 相关操作 */ +admin.categoryList = { + tablePagination: new TablePaginate("category"), + pageInfo: { + currentCount: 1, + pageCount: 1, + currentPage: 1 + }, + /* + * 初始化 table, pagination + */ + init: function(page) { + this.tablePagination.buildTable([{ + text: "", + index: "linkOrder", + width: 60 + }, { + style: "padding-left: 12px;", + text: Label.titleLabel, + index: "categoryTitle", + width: 230 + }, { + style: "padding-left: 12px;", + text: 'URI', + index: "categoryURI", + width: 230 + }, { + style: "padding-left: 12px;", + text: Label.descriptionLabel, + index: "categoryDesc", + minWidth: 180 + }]); + + this.tablePagination.initPagination(); + this.getList(page); + + // For tag auto-completion + $.ajax({// Gets all tags + url: Label.servePath + "/console/tags", + type: "GET", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + if (0 >= result.tags.length) { + return; + } + + var tags = []; + for (var i = 0; i < result.tags.length; i++) { + tags.push(result.tags[i].tagTitle); + } + + $("#categoryTags").completed({ + height: 160, + buttonText: Label.selectLabel, + data: tags + }); + + $("#loadMsg").text(""); + } + }); + }, + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function(pageNum) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + this.pageInfo.currentPage = pageNum; + var that = this; + + $.ajax({ + url: Label.servePath + "/console/categories/" + pageNum + "/" + Label.PAGE_SIZE + "/" + Label.WINDOW_SIZE, + type: "GET", + cache: false, + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var categories = result.categories; + var categoryData = []; + admin.categoryList.pageInfo.currentCount = categories.length; + admin.categoryList.pageInfo.pageCount = result.pagination.paginationPageCount === 0 ? 1 : result.pagination.paginationPageCount; + + for (var i = 0; i < categories.length; i++) { + categoryData[i] = {}; + if (i === 0) { + if (categories.length === 1) { + categoryData[i].linkOrder = ""; + } else { + categoryData[i].linkOrder = '
    \ + \ +
    '; + } + } else if (i === categories.length - 1) { + categoryData[i].linkOrder = '
    \ + \ +
    '; + } else { + categoryData[i].linkOrder = '
    \ + \ + \ +
    '; + } + + categoryData[i].categoryTitle = categories[i].categoryTitle; + categoryData[i].categoryURI = categories[i].categoryURI; + categoryData[i].categoryDesc = categories[i].categoryDescription; + + categoryData[i].expendRow = "" + Label.updateLabel + "\ + " + Label.removeLabel + " "; + + } + that.tablePagination.updateTablePagination(categoryData, pageNum, result.pagination); + $("#loadMsg").text(""); + } + }); + }, + /* + * 添加分类 + */ + add: function() { + if (this.validate()) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + var requestJSONObject = { + "categoryTitle": $("#categoryName").val(), + "categoryTags": $("#categoryTags").val(), + "categoryURI": $("#categoryURI").val(), + "categoryDescription": $("#categoryDesc").val() + }; + + var oId = $("#categoryName").data("oId"); + var type = "POST" + if (oId) { + requestJSONObject.oId = oId + type = "PUT" + } + + $.ajax({ + url: Label.servePath + "/console/category/", + type: type, + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + if (!oId) { + if (admin.categoryList.pageInfo.currentCount === Label.PAGE_SIZE && + admin.categoryList.pageInfo.currentPage === admin.categoryList.pageInfo.pageCount) { + admin.categoryList.pageInfo.pageCount++; + } + var hashList = window.location.hash.split("/"); + if (admin.categoryList.pageInfo.pageCount !== parseInt(hashList[hashList.length - 1])) { + admin.setHashByPage(admin.categoryList.pageInfo.pageCount); + } + } + + $("#categoryName").val("").data("oId", ''); + $("#categoryTags").val(""); + $("#categoryURI").val(""); + $("#categoryDesc").val(""); + + admin.categoryList.getList(admin.categoryList.pageInfo.pageCount); + + $("#loadMsg").text(""); + } + }); + } + }, + /* + * 获取单个分类 + * @id 用户 id + */ + get: function(id) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/category/" + id, + type: "GET", + cache: false, + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + $("#categoryName").val(result.categoryTitle).data("oId", id); + $("#categoryURI").val(result.categoryURI); + $("#categoryDesc").val(result.categoryDescription); + $("#categoryTags").val(result.categoryTags); + + $("#loadMsg").text(""); + } + }); + }, + /* + * 删除分类 + * @id 分类 id + * @categoryName 分类名称 + */ + del: function(id, categoryName) { + var isDelete = confirm(Label.confirmRemoveLabel + Label.categoryLabel + '"' + Util.htmlDecode(categoryName) + '"?'); + if (isDelete) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/category/" + id, + type: "DELETE", + cache: false, + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var pageNum = admin.categoryList.pageInfo.currentPage; + if (admin.categoryList.pageInfo.currentCount === 1 && admin.categoryList.pageInfo.pageCount !== 1 && + admin.categoryList.pageInfo.currentPage === admin.categoryList.pageInfo.pageCount) { + admin.categoryList.pageInfo.pageCount--; + pageNum = admin.categoryList.pageInfo.pageCount; + } + var hashList = window.location.hash.split("/"); + if (pageNum !== parseInt(hashList[hashList.length - 1])) { + admin.setHashByPage(pageNum); + } + admin.categoryList.getList(pageNum); + + $("#loadMsg").text(""); + } + }); + } + }, + /* + * 验证字段 + * @status 更新或者添加时进行验证 + */ + validate: function(status) { + if (!status) { + status = ""; + } + var categoryName = $("#categoryName" + status).val().replace(/(^\s*)|(\s*$)/g, ""); + if (2 > categoryName.length || categoryName.length > 32) { + $("#tipMsg").text(Label.categoryTooLongLabel); + $("#categoryName" + status).focus(); + } else if ($.trim($("#categoryTags" + status).val()) === "") { + $("#tipMsg").text(Label.tagsEmptyLabel); + $("#categoryTags" + status).focus(); + } else { + return true; + } + return false; + }, + /* + * 调换顺序 + */ + changeOrder: function (id, order, status) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + var requestJSONObject = { + "oId": id.toString(), + "direction": status + }; + + $.ajax({ + url: Label.servePath + "/console/category/order/", + type: "PUT", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + + // Refershes the link list + admin.categoryList.getList(admin.categoryList.pageInfo.currentPage); + + $("#loadMsg").text(""); + } + }); + } +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register["category-list"] = { + "obj": admin.categoryList, + "init": admin.categoryList.init, + "refresh": admin.categoryList.getList +} \ No newline at end of file diff --git a/src/main/webapp/js/admin/comment.js b/src/main/webapp/js/admin/comment.js new file mode 100644 index 00000000..4884fa5b --- /dev/null +++ b/src/main/webapp/js/admin/comment.js @@ -0,0 +1,137 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * common comment for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.1.0.0, Mar 17, 2019 + */ + +admin.comment = { + /* + * 打开评论窗口 + * @id 该评论对应的 id + * @fromId 该评论来自文章/草稿 + */ + open: function (id, fromId) { + this.getList(id, fromId) + $('#' + fromId + 'Comments').dialog('open') + }, + + /* + * 获取评论列表 + * + * @onId 该评论对应的文章 id + * @fromId 该评论来自文章/草稿 + */ + getList: function (onId, fromId) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + $('#' + fromId + 'Comments').html('') + + var from = 'article' + if (fromId === 'page') { + from = 'page' + } + + $.ajax({ + url: Label.servePath + '/console/comments/' + from + '/' + onId, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + var comments = result.comments, + commentsHTML = '' + for (var i = 0; i < comments.length; i++) { + var hrefHTML = '', + content = comments[i].commentContent, + contentHTML = content + + if (comments[i].commentURL === 'http://') { + hrefHTML = '' + } + + commentsHTML += '
    ' + + hrefHTML + comments[i].commentName + '' + + if (comments[i].commentOriginalCommentName) { + commentsHTML += '@' + comments[i].commentOriginalCommentName + } + commentsHTML += '' + + Label.removeLabel + '  ' + + $.bowknot.getDate(comments[i].commentTime) + + + ' 
    ' + + contentHTML + '
    ' + } + if ('' === commentsHTML) { + commentsHTML = Label.noCommentLabel + } + + $('#' + fromId + 'Comments').html(commentsHTML) + + Util.parseMarkdown() + Util.parseLanguage() + $('#loadMsg').text('') + }, + }) + }, + + /* + * 删除评论 + * @id 评论 id + * @fromId 该评论来自文章/草稿 + * @articleId 该评论对应的文章 id + */ + del: function (id, fromId, articleId) { + var isDelete = confirm(Label.confirmRemoveLabel + Label.commentLabel + '?') + if (isDelete) { + $('#loadMsg').text(Label.loadingLabel) + var from = 'article' + if (fromId === 'page') { + from = 'page' + } + + $.ajax({ + url: Label.servePath + '/console/' + from + '/comment/' + id, + type: 'DELETE', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + admin.comment.getList(articleId, fromId) + + $('#loadMsg').text('') + }, + }) + } + }, +} diff --git a/src/main/webapp/js/admin/commentList.js b/src/main/webapp/js/admin/commentList.js new file mode 100644 index 00000000..c9e63587 --- /dev/null +++ b/src/main/webapp/js/admin/commentList.js @@ -0,0 +1,157 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * comment list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.1.0.0, Mar 17, 2019 + */ + +/* comment-list 相关操作 */ +admin.commentList = { + tablePagination: new TablePaginate('comment'), + pageInfo: { + currentPage: 1, + }, + + /* + * 初始化 table, pagination, comments dialog + */ + init: function (page) { + this.tablePagination.buildTable([ + { + text: Label.commentContentLabel, + index: 'content', + minWidth: 174, + style: 'padding-left: 12px;', + }, { + text: Label.authorLabel, + index: 'title', + style: 'padding-left: 12px;', + width: 154, + }, { + text: Label.dateLabel, + index: 'date', + width: 60, + style: 'padding-left: 12px;', + }]) + this.tablePagination.initPagination() + this.getList(page) + }, + + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function (pageNum) { + var that = this + $('#loadMsg').text(Label.loadingLabel) + + $.ajax({ + url: Label.servePath + '/console/comments/' + pageNum + '/' + + Label.PAGE_SIZE + '/' + Label.WINDOW_SIZE, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + that.pageInfo.currentPage = pageNum + var comments = result.comments, + commentsData = [] + for (var i = 0; i < comments.length; i++) { + var type = 'Article' + commentsData[i] = {} + commentsData[i].content = '
    ' + + comments[i].commentContent + + '
    on   ' + comments[i].commentTitle + + '' + + commentsData[i].expendRow = '' + Label.removeLabel + + '' + + commentsData[i].title = '' + if ('http://' === comments[i].commentURL) { + commentsData[i].title += comments[i].commentName + } else { + commentsData[i].title += '' + + comments[i].commentName + + '' + } + commentsData[i].title += '
    ' + + commentsData[i].date = $.bowknot.getDate(comments[i].commentTime) + } + + that.tablePagination.updateTablePagination(commentsData, pageNum, + result.pagination) + + Util.parseMarkdown() + Util.parseLanguage() + $('#loadMsg').text('') + }, + }) + }, + + /* + * 删除评论 + * @id 评论 id + * @type 评论类型:文章 + */ + del: function (id, type) { + if (confirm(Label.confirmRemoveLabel + Label.commentLabel + '?')) { + $('#loadMsg').text(Label.loadingLabel) + + $.ajax({ + url: Label.servePath + '/console/' + type.toLowerCase() + + '/comment/' + id, + type: 'DELETE', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + admin.commentList.getList(admin.commentList.pageInfo.currentPage) + + $('#loadMsg').text('') + }, + }) + } + }, +} + +/* + * 注册到 admin 进行管理 + */ +admin.register['comment-list'] = { + 'obj': admin.commentList, + 'init': admin.commentList.init, + 'refresh': admin.commentList.getList, +} \ No newline at end of file diff --git a/src/main/webapp/js/admin/draftList.js b/src/main/webapp/js/admin/draftList.js new file mode 100644 index 00000000..e13654bb --- /dev/null +++ b/src/main/webapp/js/admin/draftList.js @@ -0,0 +1,116 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * draft list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.0.2.0, Sep 10, 2018 + */ + +/* draft-list 相关操作 */ +admin.draftList = { + tablePagination: new TablePaginate("draft"), + + /* + * 初始化 table, pagination, comments dialog + */ + init: function (page) { + this.tablePagination.buildTable([{ + text: Label.titleLabel, + index: "title", + minWidth: 110, + style: "padding-left: 12px;font-size:14px;" + }, { + text: Label.authorLabel, + index: "author", + width: 150, + style: "padding-left: 12px;" + }, { + text: Label.commentLabel, + index: "comments", + width: 80, + style: "padding-left: 12px;" + }, { + text: Label.viewLabel, + width: 60, + index: "articleViewCount", + style: "padding-left: 12px;" + }, { + text: Label.dateLabel, + index: "date", + width: 90, + style: "padding-left: 12px;" + }]); + this.tablePagination.initPagination(); + this.tablePagination.initCommentsDialog(); + this.getList(page); + }, + + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function (pageNum) { + $("#loadMsg").text(Label.loadingLabel); + var that = this; + + $.ajax({ + url: Label.servePath + "/console/articles/status/unpublished/" + pageNum + "/" + Label.PAGE_SIZE + "/" + Label.WINDOW_SIZE, + type: "GET", + cache: false, + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var articles = result.articles, + articleData = []; + for (var i = 0; i < articles.length; i++) { + articleData[i] = {}; + articleData[i].tags = articles[i].articleTags; + articleData[i].date = $.bowknot.getDate(articles[i].articleCreateTime); + articleData[i].comments = articles[i].articleCommentCount; + articleData[i].articleViewCount = articles[i].articleViewCount; + articleData[i].author = articles[i].authorName; + articleData[i].title = "" + + articles[i].articleTitle + "" + articles[i].articleTags + ""; + articleData[i].expendRow = "" + Label.updateLabel + " \ + " + Label.removeLabel + " \ + " + Label.commentLabel + ""; + } + + that.tablePagination.updateTablePagination(articleData, pageNum, result.pagination); + + $("#loadMsg").text(""); + } + }); + } +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register["draft-list"] = { + "obj": admin.draftList, + "init": admin.draftList.init, + "refresh": admin.draftList.getList +}; \ No newline at end of file diff --git a/src/main/webapp/js/admin/editor.js b/src/main/webapp/js/admin/editor.js new file mode 100644 index 00000000..36d10081 --- /dev/null +++ b/src/main/webapp/js/admin/editor.js @@ -0,0 +1,107 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @fileoverview editor + * + * @author Liyuan Li + * @version 1.3.0.1, Aug 6, 2019 + */ + +admin.editors = {} + +/* + * @description Create SoloEditor can use all editor. + * @constructor + * @param conf 编辑器初始化参数 + * @param conf.id 编辑器渲染元素 id + * @param conf.height 编辑器种类 + */ +var SoloEditor = function (conf) { + this.conf = conf + this.init() +} + +$.extend(SoloEditor.prototype, { + /* + * @description 初始化编辑器 + */ + init: function () { + this.editor = new Vditor(this.conf.id, { + cache: true, + tab: '\t', + preview: { + delay: 500, + mode: this.conf.previewMode, + url: Label.servePath + '/console/markdown/2html', + hljs: { + enable: !Label.luteAvailable, + style: Label.hljsStyle, + }, + parse: function(element) { + if (element.style.display === 'none') { + return + } + Util.parseLanguage() + }, + }, + upload: { + max: 10 * 1024 * 1024, + url: Label.uploadURL, + token: Label.uploadToken, + filename: function (name) { + return name.replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, ''). + replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, ''). + replace('/\\s/g', '') + } + }, + height: this.conf.height, + counter: 102400, + resize: { + enable: this.conf.resize, + }, + lang: Label.localeString, + }) + + if (typeof this.conf.fun === 'function') { + this.conf.fun() + } + }, + /* + * @description 获取编辑器值 + * @returns {string} 编辑器值 + */ + getContent: function () { + return this.editor.getValue() + }, + /* + * @description 设置编辑器值 + * @param {string} content 编辑器回填内容 + */ + setContent: function (content) { + this.editor.setValue(content) + }, + /* + * @description 移除编辑器值 + */ + remove: function () { + document.getElementById(this.editor.vditor.id).outerHTML = '' + }, +}) + +admin.editors.articleEditor = {} +admin.editors.abstractEditor = {} diff --git a/src/main/webapp/js/admin/linkList.js b/src/main/webapp/js/admin/linkList.js new file mode 100644 index 00000000..13882090 --- /dev/null +++ b/src/main/webapp/js/admin/linkList.js @@ -0,0 +1,353 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * link list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.1.2.1, Oct 24, 2018 + */ + +/* link-list 相关操作 */ +admin.linkList = { + tablePagination: new TablePaginate("link"), + pageInfo: { + currentCount: 1, + pageCount: 1, + currentPage: 1 + }, + id: "", + /* + * 初始化 table, pagination + */ + init: function (page) { + this.tablePagination.buildTable([{ + text: "", + index: "linkOrder", + width: 60 + },{ + style: "padding-left: 12px;", + text: Label.linkTitleLabel, + index: "linkTitle", + width: 230 + }, { + style: "padding-left: 12px;", + text: Label.urlLabel, + index: "linkAddress", + minWidth: 180 + }, { + style: "padding-left: 12px;", + text: Label.linkDescriptionLabel, + index: "linkDescription", + width: 360 + }]); + + this.tablePagination.initPagination(); + this.getList(page); + + $("#updateLink").dialog({ + title: $("#updateLink").data('title'), + width: 700, + height: 290, + "modal": true, + "hideFooter": true + }); + }, + + /* + * 根据当前页码获取链接列表 + * + * @pagNum 当前页码 + */ + getList: function (pageNum) { + $("#loadMsg").text(Label.loadingLabel); + if (pageNum === 0) { + pageNum = 1; + } + this.pageInfo.currentPage = pageNum; + var that = this; + + $.ajax({ + url: Label.servePath + "/console/links/" + pageNum + "/" + Label.PAGE_SIZE + "/" + Label.WINDOW_SIZE, + type: "GET", + cache: false, + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var links = result.links; + var linkData = []; + admin.linkList.pageInfo.currentCount = links.length; + admin.linkList.pageInfo.pageCount = result.pagination.paginationPageCount === 0 ? 1 : result.pagination.paginationPageCount; + + for (var i = 0; i < links.length; i++) { + linkData[i] = {}; + if (i === 0) { + if (links.length === 1) { + linkData[i].linkOrder = ""; + } else { + linkData[i].linkOrder = '
    \ + \ +
    '; + } + } else if (i === links.length - 1) { + linkData[i].linkOrder = '
    \ + \ +
    '; + } else { + linkData[i].linkOrder = '
    \ + \ + \ +
    '; + } + + linkData[i].linkTitle = links[i].linkTitle; + linkData[i].linkAddress = "" + + links[i].linkAddress + ""; + linkData[i].linkDescription = links[i].linkDescription; + linkData[i].expendRow = "" + Label.viewLabel + " \ + " + Label.updateLabel + "\ + " + Label.removeLabel + ""; + } + + that.tablePagination.updateTablePagination(linkData, pageNum, result.pagination); + + $("#loadMsg").text(""); + } + }); + }, + + /* + * 添加链接 + */ + add: function () { + if (this.validate()) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + var requestJSONObject = { + "link": { + "linkTitle": $("#linkTitle").val(), + "linkAddress": $("#linkAddress").val(), + "linkDescription": $("#linkDescription").val() + } + }; + + $.ajax({ + url: Label.servePath + "/console/link/", + type: "POST", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + $("#linkTitle").val(""); + $("#linkAddress").val(""); + $("#linkDescription").val(""); + if (admin.linkList.pageInfo.currentCount === Label.PAGE_SIZE && + admin.linkList.pageInfo.currentPage === admin.linkList.pageInfo.pageCount) { + admin.linkList.pageInfo.pageCount++; + } + var hashList = window.location.hash.split("/"); + if (admin.linkList.pageInfo.pageCount !== parseInt(hashList[hashList.length - 1])) { + admin.setHashByPage(admin.linkList.pageInfo.pageCount); + } + + admin.linkList.getList(admin.linkList.pageInfo.pageCount); + + $("#loadMsg").text(""); + } + }); + } + }, + + /* + * 获取链接 + * @id 链接 id + */ + get: function (id) { + $("#loadMsg").text(Label.loadingLabel); + $("#updateLink").dialog("open"); + + $.ajax({ + url: Label.servePath + "/console/link/" + id, + type: "GET", + cache: false, + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + admin.linkList.id = id; + + $("#linkTitleUpdate").val(result.link.linkTitle); + $("#linkAddressUpdate").val(result.link.linkAddress); + $("#linkDescriptionUpdate").val(result.link.linkDescription); + + $("#loadMsg").text(""); + } + }); + }, + + /* + * 更新链接 + */ + update: function () { + if (this.validate("Update")) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + var requestJSONObject = { + "link": { + "linkTitle": $("#linkTitleUpdate").val(), + "oId": this.id, + "linkAddress": $("#linkAddressUpdate").val(), + "linkDescription": $("#linkDescriptionUpdate").val() + } + }; + + $.ajax({ + url: Label.servePath + "/console/link/", + type: "PUT", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus){ + $("#updateLink").dialog("close"); + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + admin.linkList.getList(admin.linkList.pageInfo.currentPage); + + $("#loadMsg").text(""); + } + }); + } + }, + + /* + * 删除链接 + * @id 链接 id + * @title 链接标题 + */ + del: function (id, title) { + var isDelete = confirm(Label.confirmRemoveLabel + Label.permalinkLabel + '"' + Util.htmlDecode(title) + '"?'); + if (isDelete) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/link/" + id, + type: "DELETE", + cache: false, + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var pageNum = admin.linkList.pageInfo.currentPage; + if (admin.linkList.pageInfo.currentCount === 1 && admin.linkList.pageInfo.pageCount !== 1 && + admin.linkList.pageInfo.currentPage === admin.linkList.pageInfo.pageCount) { + admin.linkList.pageInfo.pageCount--; + pageNum = admin.linkList.pageInfo.pageCount; + } + + var hashList = window.location.hash.split("/"); + if (pageNum !== parseInt(hashList[hashList.length - 1])) { + admin.setHashByPage(pageNum); + } + + admin.linkList.getList(pageNum); + + $("#loadMsg").text(""); + } + }); + } + }, + + /* + * 验证字段 + * @status 更新或者添加时进行验证 + */ + validate: function (status) { + if (!status) { + status = ""; + } + if ($("#linkTitle" + status).val().replace(/\s/g, "") === "") { + $("#tipMsg").text(Label.titleEmptyLabel); + $("#linkTitle" + status).focus().val(""); + } else if ($("#linkAddress" + status).val().replace(/\s/g, "") === "") { + $("#tipMsg").text(Label.addressEmptyLabel); + $("#linkAddress" + status).focus().val(""); + } else if (!/^\w+:\/\//.test($("#linkAddress" + status).val())) { + $("#tipMsg").text(Label.addressInvalidLabel); + $("#linkAddress" + status).focus().val(""); + } else { + return true; + } + return false; + }, + + /* + * 调换顺序 + */ + changeOrder: function (id, order, status) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + var requestJSONObject = { + "oId": id.toString(), + "direction": status + }; + + $.ajax({ + url: Label.servePath + "/console/link/order/", + type: "PUT", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus){ + $("#tipMsg").text(result.msg); + + // Refershes the link list + admin.linkList.getList(admin.linkList.pageInfo.currentPage); + + $("#loadMsg").text(""); + } + }); + } +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register["link-list"] = { + "obj": admin.linkList, + "init": admin.linkList.init, + "refresh": admin.linkList.getList +} \ No newline at end of file diff --git a/src/main/webapp/js/admin/main.js b/src/main/webapp/js/admin/main.js new file mode 100644 index 00000000..3a253ea2 --- /dev/null +++ b/src/main/webapp/js/admin/main.js @@ -0,0 +1,40 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * main for admin + * + * @author Liyuan Li + * @version 1.0.0.3, May 28, 2013 + */ + +/* main 相关操作 */ +admin.main = { +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register.main = { + "obj": admin.main, + "init": function () { + admin.clearTip(); + }, + "refresh": function () { + admin.clearTip(); + } +}; diff --git a/src/main/webapp/js/admin/others.js b/src/main/webapp/js/admin/others.js new file mode 100644 index 00000000..be490487 --- /dev/null +++ b/src/main/webapp/js/admin/others.js @@ -0,0 +1,162 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * others for admin. + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.4.0.0, Mar 20, 2019 + */ + +/* others 相关操作 */ +admin.others = { + /* + * @description 初始化 + */ + init: function () { + $("#tabOthers").tabs(); + $('#loadMsg').text('') + }, + /* + * @description 移除未使用的存档 + */ + removeUnusedArchives: function () { + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/archive/unused", + type: "DELETE", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + } + }); + }, + /* + * @description 移除未使用的标签 + */ + removeUnusedTags: function () { + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/tag/unused", + type: "DELETE", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + } + }); + }, + /* + * @description 导出数据为 SQL 文件 + */ + exportSQL: function () { + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/export/sql", + type: "GET", + cache: false, + success: function (result, textStatus) { + // AJAX 下载文件的话这里会发两次请求,用 sc 来判断是否是文件,如果没有 sc 说明文件可以下载(实际上就是 result) + if (!result.sc) { + // 再发一次请求进行正式下载 + window.location = Label.servePath + "/console/export/sql"; + } else { + $("#tipMsg").text(result.msg); + } + } + }); + }, + /* + * @description 导出数据为 JSON 文件 + */ + exportJSON: function () { + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/export/json", + type: "GET", + cache: false, + success: function (result, textStatus) { + // AJAX 下载文件的话这里会发两次请求,用 sc 来判断是否是文件,如果没有 sc 说明文件可以下载(实际上就是 result) + if (!result.sc) { + // 再发一次请求进行正式下载 + window.location = Label.servePath + "/console/export/json"; + } else { + $("#tipMsg").text(result.msg); + } + } + }); + }, + /* + * @description 导出数据为 Hexo Markdown 文件 + */ + exportHexo: function () { + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/export/hexo", + type: "GET", + cache: false, + success: function (result, textStatus) { + // AJAX 下载文件的话这里会发两次请求,用 sc 来判断是否是文件,如果没有 sc 说明文件可以下载(实际上就是 result) + if (!result.sc) { + // 再发一次请求进行正式下载 + window.location = Label.servePath + "/console/export/hexo"; + } else { + $("#tipMsg").text(result.msg); + } + } + }); + }, + /* + * 获取未使用的标签。 + * XXX: Not used this function yet. + */ + getUnusedTags: function () { + $.ajax({ + url: Label.servePath + "/console/tag/unused", + type: "GET", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var unusedTags = result.unusedTags; + if (0 === unusedTags.length) { + return; + } + } + }); + } +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register.others = { + "obj": admin.others, + "init": admin.others.init, + "refresh": function () { + admin.clearTip(); + } +}; diff --git a/src/main/webapp/js/admin/pageList.js b/src/main/webapp/js/admin/pageList.js new file mode 100644 index 00000000..163b1ed8 --- /dev/null +++ b/src/main/webapp/js/admin/pageList.js @@ -0,0 +1,354 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * page list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.3.0.1, Apr 22, 2019 + */ + +/* page-list 相关操作 */ +admin.pageList = { + tablePagination: new TablePaginate("page"), + pageInfo: { + currentCount: 1, + pageCount: 1, + currentPage: 1 + }, + id: "", + /* + * 初始化 table, pagination + */ + init: function (page) { + this.tablePagination.buildTable([{ + text: "", + index: "pageOrder", + width: 60, + style: "padding-left: 12px;font-size:14px;" + }, { + style: "padding-left: 12px;", + text: Label.titleLabel, + index: "pageTitle", + width: 300 + }, { + style: "padding-left: 12px;", + text: Label.permalinkLabel, + index: "pagePermalink", + minWidth: 100 + }, { + style: "padding-left: 12px;", + text: Label.openMethodLabel, + index: "pageTarget", + width: 120 + }]); + this.tablePagination.initPagination(); + this.getList(page); + }, + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function (pageNum) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + var that = this; + + $.ajax({ + url: Label.servePath + "/console/pages/" + pageNum + "/" + Label.PAGE_SIZE + "/" + Label.WINDOW_SIZE, + type: "GET", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var pages = result.pages; + var pageData = []; + admin.pageList.pageInfo.currentCount = pages.length; + admin.pageList.pageInfo.pageCount = result.pagination.paginationPageCount === 0 ? 1 : result.pagination.paginationPageCount; + for (var i = 0; i < pages.length; i++) { + pageData[i] = {}; + if (i === 0) { + if (pages.length === 1) { + pageData[i].pageOrder = ""; + } else { + pageData[i].pageOrder = '
    \ +
    '; + } + } else if (i === pages.length - 1) { + pageData[i].pageOrder = '
    \ + \ +
    '; + } else { + pageData[i].pageOrder = '
    \ + \ + \ +
    '; + } + + var pageIcon = ''; + if (pages[i].pageIcon !== '') { + pageIcon = " "; + } + pageData[i].pageTitle = pageIcon + "" + + pages[i].pageTitle + ""; + pageData[i].pagePermalink = "" + + pages[i].pagePermalink + ""; + pageData[i].pageTarget = pages[i].pageOpenTarget; + pageData[i].expendRow = "" + Label.viewLabel + " \ + " + Label.updateLabel + "\ + " + Label.removeLabel + ""; + } + + that.tablePagination.updateTablePagination(pageData, pageNum, result.pagination); + + $("#loadMsg").text(""); + } + }); + }, + /* + * 获取自定义页面 + * @id 自定义页面 id + */ + get: function (id) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/page/" + id, + type: "GET", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + admin.pageList.id = id; + + $("#pageTitle").val(result.page.pageTitle); + $("#pagePermalink").val(result.page.pagePermalink); + $("#pageTarget").val(result.page.pageOpenTarget); + $("#pageIcon").val(result.page.pageIcon); + $("#loadMsg").text(""); + } + }); + }, + /* + * 删除自定义页面 + * @id 自定义页面 id + * @title 自定义页面标题 + */ + del: function (id, title) { + var isDelete = confirm(Label.confirmRemoveLabel + Label.navLabel + '"' + Util.htmlDecode(title) + '"?'); + if (isDelete) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + $.ajax({ + url: Label.servePath + "/console/page/" + id, + type: "DELETE", + cache: false, + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + var pageNum = admin.pageList.pageInfo.currentPage; + if (admin.pageList.pageInfo.currentCount === 1 && admin.pageList.pageInfo.pageCount !== 1 && + admin.pageList.pageInfo.currentPage === admin.pageList.pageInfo.pageCount) { + admin.pageList.pageInfo.pageCount--; + pageNum = admin.pageList.pageInfo.pageCount; + } + var hashList = window.location.hash.split("/"); + if (pageNum == hashList[hashList.length - 1]) { + admin.pageList.getList(pageNum); + } else { + admin.setHashByPage(pageNum); + } + + $("#loadMsg").text(""); + } + }); + } + }, + /* + * 添加自定义页面 + */ + add: function () { + if (this.validate()) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + var pagePermalink = $("#pagePermalink").val().replace(/(^\s*)|(\s*$)/g, ""); + + var requestJSONObject = { + "page": { + "pageTitle": $("#pageTitle").val(), + "pagePermalink": pagePermalink, + "pageOpenTarget": $("#pageTarget").val(), + "pageIcon": $("#pageIcon").val() + } + }; + + $.ajax({ + url: Label.servePath + "/console/page/", + type: "POST", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + admin.pageList.id = ""; + $("#pagePermalink").val(""); + $("#pageTitle").val(""); + $("#pageIcon").val(""); + $("#pageTarget").val("_self"); + + if (admin.pageList.pageInfo.currentCount === Label.PAGE_SIZE && + admin.pageList.pageInfo.currentPage === admin.pageList.pageInfo.pageCount) { + admin.pageList.pageInfo.pageCount++; + } + var hashList = window.location.hash.split("/"); + if (admin.pageList.pageInfo.pageCount == hashList[hashList.length - 1]) { + admin.pageList.getList(admin.pageList.pageInfo.pageCount); + } else { + admin.setHashByPage(admin.pageList.pageInfo.pageCount); + } + + $("#loadMsg").text(""); + } + }); + } + }, + /* + * 更新自定义页面 + */ + update: function () { + if (this.validate()) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + var pagePermalink = $("#pagePermalink").val().replace(/(^\s*)|(\s*$)/g, ""); + + var requestJSONObject = { + "page": { + "pageTitle": $("#pageTitle").val(), + "oId": this.id, + "pagePermalink": pagePermalink, + "pageOpenTarget": $("#pageTarget").val(), + "pageIcon": $("#pageIcon").val() + } + }; + + $.ajax({ + url: Label.servePath + "/console/page/", + type: "PUT", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + admin.pageList.id = ""; + + admin.pageList.getList(admin.pageList.pageInfo.currentPage); + $("#pageTitle").val(""); + $("#pageIcon").val(""); + $("#pagePermalink").val(""); + $("#pageTarget").val("_self"); + $("#loadMsg").text(""); + } + }); + } + }, + /* + * 验证字段 + */ + validate: function () { + if ($("#pageTitle").val().replace(/\s/g, "") === "") { + $("#tipMsg").text(Label.titleEmptyLabel); + $("#pageTitle").focus(); + } else if ($("#pagePermalink").val().replace(/\s/g, "") === "") { + $("#tipMsg").text(Label.linkEmptyLabel); + } else { + return true; + } + return false; + }, + /* + * 提交自定义页面 + */ + submit: function () { + if (this.id !== "") { + this.update(); + } else { + this.add(); + } + }, + /* + * 调换顺序 + */ + changeOrder: function (id, order, status) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + + var requestJSONObject = { + "oId": id.toString(), + "direction": status + }; + + $.ajax({ + url: Label.servePath + "/console/page/order/", + type: "PUT", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $("#tipMsg").text(result.msg); + + // Refershes the page list + admin.pageList.getList(admin.pageList.pageInfo.currentPage); + + $("#loadMsg").text(""); + } + }); + } +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register["page-list"] = { + "obj": admin.pageList, + "init": admin.pageList.init, + "refresh": admin.pageList.getList +} diff --git a/src/main/webapp/js/admin/plugin.js b/src/main/webapp/js/admin/plugin.js new file mode 100644 index 00000000..fd2836e1 --- /dev/null +++ b/src/main/webapp/js/admin/plugin.js @@ -0,0 +1,136 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * plugin manager for admin + * + * @author Liyuan Li + * @version 1.0.0.7, Mar 28, 2012 + */ +var plugins = {}; +admin.plugin = { + plugins: [], + + /* + * 添加插件进行管理 + */ + add: function (data) { + // 添加所有插件 + data.isInit = false; + data.hash = data.path.replace("/", "#") + "/" + data.id; + this.plugins.push(data); + + var pathList = this._analysePath(data.path); + // 添加一二级 Tab + if (data.index && pathList.length < 2) { + this._addNew(data, pathList); + } + }, + + /* + * 根据当前 hash 初始化或刷新插件 + */ + setCurByHash: function (tags) { + var pluginList = this.plugins; + for (var i = 0; i < pluginList.length; i++) { + var data = pluginList[i]; + var pathList = this._analysePath(data.path), + isCurrentPlugin = false; + + // 根据当前 hash 和插件 path 判别是非为当前插件 + if (data.index && window.location.hash.indexOf(data.hash) > -1) { + isCurrentPlugin = true; + } else if(data.path.replace("/", "#") === window.location.hash || + (window.location.hash === "#main" && data.path.indexOf("/main/panel") > -1)) { + isCurrentPlugin = true; + } + + if (isCurrentPlugin) { + if (data.isInit) { + // 插件已经初始化过,只需进行刷新 + if (plugins[data.id].refresh) { + plugins[data.id].refresh(tags.page); + } + } else { + // 初始化插件 + if (!data.index){ + this._addToExist(data, pathList); + } else if (pathList.length === 2) { + this._addNew(data, pathList); + } + plugins[data.id].init(tags.page); + data.isInit = true; + } + } + } + }, + + /* + * 解析添加路径 + */ + _analysePath: function (path) { + var paths = path.split("/"); + paths.splice(0, 1); + return paths; + }, + + /* + * 添加一二级 tab + */ + _addNew: function (data, pathList) { + if (pathList.length === 2) { + data.target = $("#tabPreference li").get(data.index - 1); + $("#tabPreference").tabs("add", data); + return; + } else if (pathList[0] === "") { + data.target = $("#tabs>ul>li").get(data.index - 1); + } else if (pathList[0] === "article") { + data.target = $("#tabArticleMgt>li").get(data.index - 1); + } else if (pathList[0] === "tools") { + admin.tools.push("#" + data.id); + data.target = $("#tabTools>li").get(data.index - 1); + } + + if (!data.target) { + alert("data.index is error!"); + } + + $("#tabs").tabs("add", data); + }, + + /* + * 在已有页面上进行添加 + */ + _addToExist: function (data, pathList) { + switch (pathList[0]) { + case "main": + $("#mainPanel" + pathList[1].charAt(5)).append(data.content); + break; + case "tools": + case "article": + if (pathList.length === 2) { + $("#tabsPanel_" + pathList[1]).append(data.content); + } else { + $("#tabPreferencePanel_" + pathList[2]).append(data.content); + } + break; + case "comment-list": + $("#tabsPanel_comment-list").append(data.content); + break; + } + } +}; diff --git a/src/main/webapp/js/admin/pluginList.js b/src/main/webapp/js/admin/pluginList.js new file mode 100644 index 00000000..4b7d69af --- /dev/null +++ b/src/main/webapp/js/admin/pluginList.js @@ -0,0 +1,178 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * plugin list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.0.1.5, Apr 5, 2018 + */ + +/* plugin-list 相关操作 */ +admin.pluginList = { + tablePagination: new TablePaginate("plugin"), + pageInfo: { + currentCount: 1, + pageCount: 1, + currentPage: 1 + }, + /* + * 初始化 table, pagination + */ + init: function(page) { + this.tablePagination.buildTable([{ + style: "padding-left: 12px;", + text: Label.pluginNameLabel, + index: "name", + width: 230 + }, { + style: "padding-left: 12px;", + text: Label.statusLabel, + index: "status", + minWidth: 80 + }, { + style: "padding-left: 12px;", + text: Label.authorLabel, + index: "author", + width: 200 + }, { + style: "padding-left: 12px;", + text: Label.versionLabel, + index: "version", + width: 120 + }]); + + this.tablePagination.initPagination(); + $("#pluginSetting").dialog({ + width: 700, + height: 180, + "modal": true, + "hideFooter": true + }); + this.getList(page); + }, + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function(pageNum) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + var that = this; + + $.ajax({ + url: Label.servePath + "/console/plugins/" + pageNum + "/" + Label.PAGE_SIZE + "/" + Label.WINDOW_SIZE, + type: "GET", + cache: false, + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + admin.pluginList.pageInfo.currentPage = pageNum; + var datas = result.plugins; + for (var i = 0; i < datas.length; i++) { + datas[i].expendRow = ""; + if (datas[i].status === "ENABLED") { + datas[i].status = Label.enabledLabel; + datas[i].expendRow += Label.disableLabel; + } else { + datas[i].status = Label.disabledLabel; + datas[i].expendRow += Label.enableLabel; + } + datas[i].expendRow += " "; + + if (datas[i].setting != "{}") { + datas[i].expendRow += " " + Label.settingLabel + " "; + } + } + + that.tablePagination.updateTablePagination(result.plugins, pageNum, result.pagination); + + $("#loadMsg").text(""); + } + }); + }, + toSetting: function(pluginId) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + var requestJSONObject = { + "oId": pluginId + }; + + $.ajax({ + url: Label.servePath + "/console/plugin/toSetting", + type: "POST", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + + $("#pluginSetting").html(result); + $("#pluginSetting").dialog("open"); + + $("#loadMsg").text(""); + } + }); + }, + changeStatus: function(pluginId, status) { + $("#loadMsg").text(Label.loadingLabel); + $("#tipMsg").text(""); + if (status === "ENABLED") { + status = "DISABLED"; + } else { + status = "ENABLED"; + } + + var requestJSONObject = { + "oId": pluginId, + "status": status + }; + + $.ajax({ + url: Label.servePath + "/console/plugin/status/", + type: "PUT", + cache: false, + data: JSON.stringify(requestJSONObject), + success: function(result, textStatus) { + $("#tipMsg").text(result.msg); + if (!result.sc) { + $("#loadMsg").text(""); + return; + } + + $("#loadMsg").text(""); + window.location.reload(); + } + }); + } +}; + +/* + * 注册到 admin 进行管理 + */ +admin.register["plugin-list"] = { + "obj": admin.pluginList, + "init": admin.pluginList.init, + "refresh": function() { + $("#loadMsg").text(""); + } +}; diff --git a/src/main/webapp/js/admin/preference.js b/src/main/webapp/js/admin/preference.js new file mode 100644 index 00000000..a5efdb4e --- /dev/null +++ b/src/main/webapp/js/admin/preference.js @@ -0,0 +1,265 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * preference for admin. + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.3.0.3, Aug 18, 2019 + */ + +/* preference 相关操作 */ +admin.preference = { + locale: '', + /* + * 初始化 + */ + init: function () { + $('#tabPreference').tabs() + + $.ajax({ + url: Label.servePath + '/console/preference/', + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + var preference = result.preference + + $('#metaKeywords').val(preference.metaKeywords) + $('#metaDescription').val(preference.metaDescription) + $('#blogTitle').val(preference.blogTitle) + $('#blogSubtitle').val(preference.blogSubtitle) + $('#mostCommentArticleDisplayCount'). + val(preference.mostCommentArticleDisplayCount) + $('#mostViewArticleDisplayCount'). + val(preference.mostViewArticleDisplayCount) + $('#recentCommentDisplayCount'). + val(preference.recentCommentDisplayCount) + $('#mostUsedTagDisplayCount').val(preference.mostUsedTagDisplayCount) + $('#articleListDisplayCount').val(preference.articleListDisplayCount) + $('#articleListPaginationWindowSize'). + val(preference.articleListPaginationWindowSize) + $('#localeString').val(preference.localeString) + $('#timeZoneId').val(preference.timeZoneId) + $('#noticeBoard').val(preference.noticeBoard) + $('#footerContent').val(preference.footerContent) + $('#htmlHead').val(preference.htmlHead) + $('#externalRelevantArticlesDisplayCount'). + val(preference.externalRelevantArticlesDisplayCount) + $('#relevantArticlesDisplayCount'). + val(preference.relevantArticlesDisplayCount) + $('#randomArticlesDisplayCount'). + val(preference.randomArticlesDisplayCount) + $('#customVars').val(preference.customVars) + + 'true' === preference.enableArticleUpdateHint ? $('#enableArticleUpdateHint').attr('checked', 'checked') : $('#enableArticleUpdateHint').removeAttr('checked') + 'true' === preference.allowVisitDraftViaPermalink ? $('#allowVisitDraftViaPermalink').attr('checked', 'checked') : $('allowVisitDraftViaPermalink').removeAttr('checked') + 'true' === preference.commentable ? $('#commentable').attr('checked', 'checked') : $('commentable').removeAttr('checked') + 'true' === preference.syncGitHub ? $('#syncGitHub').attr('checked', 'checked') : $('syncGitHub').removeAttr('checked') + 'true' === preference.pullGitHub ? $('#pullGitHub').attr('checked', 'checked') : $('pullGitHub').removeAttr('checked') + + admin.preference.locale = preference.localeString + + // sign + var signs = eval('(' + preference.signs + ')') + for (var j = 1; j < signs.length; j++) { + $('#preferenceSign' + j).val(signs[j].signHTML) + } + + $('#articleListDisplay').val(preference.articleListStyle) + $('#hljsTheme').val(preference.hljsTheme) + $('#feedOutputMode').val(preference.feedOutputMode) + $('#feedOutputCnt').val(preference.feedOutputCnt) + $('#faviconURL').val(preference.faviconURL) + + $('#loadMsg').text('') + }, + }) + }, + /* + * @description 参数校验 + */ + validate: function () { + if (!/^\d+$/.test($('#mostUsedTagDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.indexTagDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#mostUsedTagDisplayCount').focus() + return false + } else if (!/^\d+$/.test($('#recentCommentDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.indexRecentCommentDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#recentCommentDisplayCount').focus() + return false + } else if (!/^\d+$/.test($('#mostCommentArticleDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.indexMostCommentArticleDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#mostCommentArticleDisplayCount').focus() + return false + } else if (!/^\d+$/.test($('#mostViewArticleDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.indexMostViewArticleDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#mostViewArticleDisplayCount').focus() + return false + } else if (!/^\d+$/.test($('#articleListDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + Label.pageSizeLabel + + '] ' + Label.nonNegativeIntegerOnlyLabel) + $('#articleListDisplayCount').focus() + return false + } else if (!/^\d+$/.test($('#articleListPaginationWindowSize').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + Label.windowSizeLabel + + '] ' + Label.nonNegativeIntegerOnlyLabel) + $('#articleListPaginationWindowSize').focus() + return false + } else if (!/^\d+$/.test($('#randomArticlesDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.randomArticlesDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#randomArticlesDisplayCount').focus() + return false + } else if (!/^\d+$/.test($('#relevantArticlesDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.relevantArticlesDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#relevantArticlesDisplayCount').focus() + return false + } else if (!/^\d+$/.test( + $('#externalRelevantArticlesDisplayCount').val())) { + $('#tipMsg'). + text('[' + Label.paramSettingsLabel + ' - ' + + Label.externalRelevantArticlesDisplayCntLabel + '] ' + + Label.nonNegativeIntegerOnlyLabel) + $('#externalRelevantArticlesDisplayCount').focus() + return false + } + return true + }, + /* + * @description 更新 + */ + update: function () { + if (!admin.preference.validate()) { + return + } + + $('#tipMsg').text('') + $('#loadMsg').text(Label.loadingLabel) + var signs = [ + { + 'oId': 0, + 'signHTML': '', + }, { + 'oId': 1, + 'signHTML': $('#preferenceSign1').val(), + }, { + 'oId': 2, + 'signHTML': $('#preferenceSign2').val(), + }, { + 'oId': 3, + 'signHTML': $('#preferenceSign3').val(), + }] + + var requestJSONObject = { + 'preference': { + 'metaKeywords': $('#metaKeywords').val(), + 'metaDescription': $('#metaDescription').val(), + 'blogTitle': $('#blogTitle').val(), + 'blogSubtitle': $('#blogSubtitle').val(), + 'mostCommentArticleDisplayCount': $('#mostCommentArticleDisplayCount'). + val(), + 'mostViewArticleDisplayCount': $('#mostViewArticleDisplayCount').val(), + 'recentCommentDisplayCount': $('#recentCommentDisplayCount').val(), + 'mostUsedTagDisplayCount': $('#mostUsedTagDisplayCount').val(), + 'articleListDisplayCount': $('#articleListDisplayCount').val(), + 'articleListPaginationWindowSize': $( + '#articleListPaginationWindowSize').val(), + 'localeString': $('#localeString').val(), + 'timeZoneId': $('#timeZoneId').val(), + 'noticeBoard': $('#noticeBoard').val(), + 'footerContent': $('#footerContent').val(), + 'htmlHead': $('#htmlHead').val(), + 'externalRelevantArticlesDisplayCount': $( + '#externalRelevantArticlesDisplayCount').val(), + 'relevantArticlesDisplayCount': $('#relevantArticlesDisplayCount'). + val(), + 'randomArticlesDisplayCount': $('#randomArticlesDisplayCount').val(), + 'enableArticleUpdateHint': $('#enableArticleUpdateHint'). + prop('checked'), + 'signs': signs, + 'allowVisitDraftViaPermalink': $('#allowVisitDraftViaPermalink'). + prop('checked'), + 'articleListStyle': $('#articleListDisplay').val(), + 'hljsTheme': $('#hljsTheme').val(), + 'feedOutputMode': $('#feedOutputMode').val(), + 'feedOutputCnt': $('#feedOutputCnt').val(), + 'faviconURL': $('#faviconURL').val(), + 'syncGitHub': $('#syncGitHub').prop('checked'), + 'pullGitHub': $('#pullGitHub').prop('checked'), + 'commentable': $('#commentable').prop('checked'), + 'customVars': $('#customVars').val(), + }, + } + + $.ajax({ + url: Label.servePath + '/console/preference/', + type: 'PUT', + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + if ($('#localeString').val() !== admin.preference.locale) { + window.location.reload() + } + + $('#loadMsg').text('') + }, + }) + }, +} + +/* + * 注册到 admin 进行管理 + */ +admin.register['preference'] = { + 'obj': admin.preference, + 'init': admin.preference.init, + 'refresh': function () { + admin.clearTip() + }, +} diff --git a/src/main/webapp/js/admin/tablePaginate.js b/src/main/webapp/js/admin/tablePaginate.js new file mode 100644 index 00000000..6984a310 --- /dev/null +++ b/src/main/webapp/js/admin/tablePaginate.js @@ -0,0 +1,116 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * table and paginate util + * + * @author Liyuan Li + * @version 1.0.0.8, Jun 11, 2012 + */ + +var TablePaginate = function (id) { + this.id = id; + this.currentPage = 1; +}; + +$.extend(TablePaginate.prototype, { + /* + * 构建 table 框架 + * @colModel table 列宽,标题等数据 + */ + buildTable: function (colModel, noExpend) { + var tableData = { + colModel: colModel, + noDataTip: Label.noDataLabel + } + if (!noExpend) { + tableData.expendRow = { + index: "expendRow" + } + } + $("#" + this.id + "Table").table(tableData); + + }, + + /* + * 初始化分页 + */ + initPagination: function () { + var id = this.id; + $("#" + id + "Pagination").paginate({ + "bind": function(currentPage, errorMessage) { + if (errorMessage) { + $("#tipMsg").text(errorMessage); + } else { + admin.setHashByPage(currentPage); + } + }, + "currentPage": 1, + "errorMessage": Label.inputErrorLabel, + "nextPageText": '>', + "previousPageText": '<', + "goText": Label.gotoLabel, + "type": "custom", + "custom": [1], + "pageCount": 1 + }); + }, + + /* + * 初始化评论对话框 + */ + initCommentsDialog: function () { + var that = this; + $("#" + this.id + "Comments").dialog({ + "modal": true, + "hideFooter": true, + "close": function () { + admin[that.id + "List"].getList(that.currentPage); + return true; + } + }); + }, + + /* + * 更新 table & paginateion + */ + updateTablePagination: function (data, currentPage, pageInfo) { + currentPage = parseInt(currentPage); + if (currentPage > pageInfo.paginationPageCount && currentPage > 1) { + $("#tipMsg").text(Label.pageLabel + currentPage + Label.notFoundLabel); + $("#loadMsg").text(""); + return; + } + $("#" + this.id + "Table").table("update", { + data: [{ + groupName: "all", + groupData: data + }] + }); + + if (pageInfo.paginationPageCount === 0) { + pageInfo.paginationPageCount = 1; + } + + $("#" + this.id + "Pagination").paginate("update", { + pageCount: pageInfo.paginationPageCount, + currentPage: currentPage, + custom: pageInfo.paginationPageNums + }); + this.currentPage = currentPage; + } +}); diff --git a/src/main/webapp/js/admin/themeList.js b/src/main/webapp/js/admin/themeList.js new file mode 100644 index 00000000..36ddc2ca --- /dev/null +++ b/src/main/webapp/js/admin/themeList.js @@ -0,0 +1,138 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * preference for admin. + * + * @author Liyuan Li + * @version 0.1.0.0, Mar 29, 2019 + */ + +/* theme list 相关操作 */ +admin.themeList = { + skinDirName: '', + mobileSkinDirName: '', + /* + * 初始化 + */ + init: function () { + $.ajax({ + url: Label.servePath + '/console/skin', + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + admin.themeList.skinDirName = result.skin.skinDirName + admin.themeList.mobileSkinDirName = result.skin.mobileSkinDirName + + var skins = JSON.parse(result.skin.skins) + var skinsHTML = '' + + for (var i = 0; i < skins.length; i++) { + var selectedClass = '' + if (skins[i].skinDirName === result.skin.skinDirName) { + selectedClass = ' selected' + } + + skinsHTML += '
    ' + skins[i].skinDirName + + '
    ' + + if (skins[i].skinDirName !== result.skin.skinDirName) { + skinsHTML += '' + } + + if (skins[i].skinDirName !== result.skin.mobileSkinDirName) { + skinsHTML += '' + } + + skinsHTML += '
    ' + } + $('#skinMain').html(skinsHTML + '
    ') + + $('.skinItem .update').click(function () { + admin.themeList.update($(this).data('name'), 'pc') + }) + $('.skinItem .mobile').click(function () { + admin.themeList.update($(this).data('name'), 'mobile') + }) + + $('#loadMsg').text('') + }, + }) + }, + /* + * @description 更新 + */ + update: function (skinDirName, type) { + $('#tipMsg').text('') + $('#loadMsg').text(Label.loadingLabel) + + var requestJSONObject = { + skin: { + skinDirName: admin.themeList.skinDirName, + mobileSkinDirName: admin.themeList.mobileSkinDirName, + }, + } + + if (type === 'pc') { + requestJSONObject.skin.skinDirName = skinDirName + } else { + requestJSONObject.skin.mobileSkinDirName = skinDirName + } + + $.ajax({ + url: Label.servePath + '/console/skin', + type: 'PUT', + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + admin.themeList.init() + $('#loadMsg').text('') + }, + }) + }, +} + +/* + * 注册到 admin 进行管理 + */ +admin.register['theme-list'] = { + 'obj': admin.themeList, + 'init': admin.themeList.init, + 'refresh': function () { + $('#loadMsg').text('') + }, +} diff --git a/src/main/webapp/js/admin/userList.js b/src/main/webapp/js/admin/userList.js new file mode 100644 index 00000000..b89cde41 --- /dev/null +++ b/src/main/webapp/js/admin/userList.js @@ -0,0 +1,305 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * user list for admin + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.1.2.6, Mar 3, 2019 + */ + +/* user-list 相关操作 */ +admin.userList = { + tablePagination: new TablePaginate('user'), + pageInfo: { + currentCount: 1, + pageCount: 1, + currentPage: 1, + }, + userInfo: { + 'oId': '', + 'userRole': '', + }, + /* + * 初始化 table, pagination + */ + init: function (page) { + this.tablePagination.buildTable([ + { + style: 'padding-left: 12px;', + text: Label.userNameLabel, + index: 'userName', + width: 230, + }, { + style: 'padding-left: 12px;', + text: Label.roleLabel, + index: 'isAdmin', + width: 120, + }]) + + this.tablePagination.initPagination() + this.getList(page) + + $('#userUpdate').dialog({ + width: 700, + height: 450, + 'modal': true, + 'hideFooter': true, + }) + }, + /* + * 根据当前页码获取列表 + * @pagNum 当前页码 + */ + getList: function (pageNum) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + this.pageInfo.currentPage = pageNum + var that = this + + $.ajax({ + url: Label.servePath + '/console/users/' + pageNum + '/' + + Label.PAGE_SIZE + '/' + Label.WINDOW_SIZE, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + var users = result.users + var userData = [] + admin.userList.pageInfo.currentCount = users.length + admin.userList.pageInfo.pageCount = result.pagination.paginationPageCount + if (users.length < 1) { + $('#tipMsg').text('No user ' + Label.reportIssueLabel) + $('#loadMsg').text('') + return + } + $('#tipMsg').text(Label.uploadMsg) + + for (var i = 0; i < users.length; i++) { + userData[i] = {} + userData[i].userName = users[i].userName + + if ('adminRole' === users[i].userRole) { + userData[i].isAdmin = ' ' + Label.administratorLabel + userData[i].expendRow = '' + + Label.updateLabel + '' + } else { + userData[i].expendRow = '' + + Label.updateLabel + '\ + ' + Label.removeLabel + ' ' + + '' + Label.changeRoleLabel + '' + if ('defaultRole' === users[i].userRole) { + userData[i].isAdmin = Label.commonUserLabel + } else { + userData[i].isAdmin = Label.visitorUserLabel + } + } + + that.tablePagination.updateTablePagination(userData, pageNum, + result.pagination) + + } + $('#loadMsg').text('') + }, + }) + }, + /* + * 获取用户 + * @id 用户 id + */ + get: function (id, userRole) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + $('#userUpdate').dialog('open') + + $.ajax({ + url: Label.servePath + '/console/user/' + id, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + $('#userURLUpdate').val(result.user.userURL) + $('#userAvatarUpdate').val(result.user.userAvatar) + $('#userB3KeyUpdate').val(result.user.userB3Key) + $('#userNameUpdate').val(result.user.userName).data('userInfo', { + 'oId': id, + 'userRole': userRole, + }) + + $('#loadMsg').text('') + }, + }) + }, + /* + * 更新用户 + */ + update: function () { + if (this.validate('Update')) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + + var userInfo = $('#userNameUpdate').data('userInfo') + var requestJSONObject = { + 'userName': $('#userNameUpdate').val(), + 'oId': userInfo.oId, + 'userURL': $('#userURLUpdate').val(), + 'userRole': userInfo.userRole, + 'userAvatar': $('#userAvatarUpdate').val(), + 'userB3Key': $('#userB3KeyUpdate').val(), + } + + $.ajax({ + url: Label.servePath + '/console/user/', + type: 'PUT', + cache: false, + data: JSON.stringify(requestJSONObject), + success: function (result, textStatus) { + $('#userUpdate').dialog('close') + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + admin.userList.getList(admin.userList.pageInfo.currentPage) + + $('#loadMsg').text('') + }, + }) + } + }, + /* + * 删除用户 + * @id 用户 id + * @userName 用户名称 + */ + del: function (id, userName) { + var isDelete = confirm(Label.confirmRemoveLabel + Label.userLabel + '"' + + Util.htmlDecode(userName) + '"?') + if (isDelete) { + $('#loadMsg').text(Label.loadingLabel) + $('#tipMsg').text('') + + $.ajax({ + url: Label.servePath + '/console/user/' + id, + type: 'DELETE', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + var pageNum = admin.userList.pageInfo.currentPage + if (admin.userList.pageInfo.currentCount === 1 && + admin.userList.pageInfo.pageCount !== 1 && + admin.userList.pageInfo.currentPage === + admin.userList.pageInfo.pageCount) { + admin.userList.pageInfo.pageCount-- + pageNum = admin.userList.pageInfo.pageCount + } + var hashList = window.location.hash.split('/') + if (pageNum !== parseInt(hashList[hashList.length - 1])) { + admin.setHashByPage(pageNum) + } + admin.userList.getList(pageNum) + + $('#loadMsg').text('') + }, + }) + } + }, + /** + * 修改角色 + * @param id + */ + changeRole: function (id) { + $('#tipMsg').text('') + $.ajax({ + url: Label.servePath + '/console/changeRole/' + id, + type: 'GET', + cache: false, + success: function (result, textStatus) { + $('#tipMsg').text(result.msg) + if (!result.sc) { + $('#loadMsg').text('') + return + } + + var pageNum = admin.userList.pageInfo.currentPage + if (admin.userList.pageInfo.currentCount === 1 && + admin.userList.pageInfo.pageCount !== 1 && + admin.userList.pageInfo.currentPage === + admin.userList.pageInfo.pageCount) { + admin.userList.pageInfo.pageCount-- + pageNum = admin.userList.pageInfo.pageCount + } + var hashList = window.location.hash.split('/') + if (pageNum !== parseInt(hashList[hashList.length - 1])) { + admin.setHashByPage(pageNum) + } + admin.userList.getList(pageNum) + + $('#loadMsg').text('') + }, + }) + }, + /* + * 验证字段 + * @status 更新或者添加时进行验证 + */ + validate: function (status) { + if (!status) { + status = '' + } + var userName = $('#userName' + status).val().replace(/(^\s*)|(\s*$)/g, '') + if (2 > userName.length || userName.length > 20) { + $('#tipMsg').text(Label.nameTooLongLabel) + $('#userName' + status).focus() + } else { + return true + } + + return false + }, +} + +/* + * 注册到 admin 进行管理 + */ +admin.register['user-list'] = { + 'obj': admin.userList, + 'init': admin.userList.init, + 'refresh': admin.userList.getList, +} \ No newline at end of file diff --git a/src/main/webapp/js/common.js b/src/main/webapp/js/common.js new file mode 100644 index 00000000..0eff3f97 --- /dev/null +++ b/src/main/webapp/js/common.js @@ -0,0 +1,404 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @fileoverview util and every page should be used. + * + * @author Liyuan Li + * @author Liang Ding + * @version 1.9.1.0, Sep 9, 2019 + */ + +/** + * @description Util + * @static + */ +var Util = { + isArticlePage: function (href) { + var isArticle = true + if (href.indexOf(Label.servePath + '/tags/') > -1) { + isArticle = false + } + if (href.indexOf(Label.servePath + '/tags.html') > -1) { + isArticle = false + } + if (href.indexOf(Label.servePath + '/category/') > -1) { + isArticle = false + } + if (href.indexOf(Label.servePath + '/archives.html') > -1) { + isArticle = false + } + if (href.indexOf(Label.servePath + '/archives/') > -1) { + isArticle = false + } + if (href.indexOf(Label.servePath + '/links.html') > -1) { + isArticle = false + } + if (href === Label.servePath) { + isArticle = false + } + if (/^[0-9]*$/.test(href.replace(Label.servePath + '/', ''))) { + isArticle = false + } + return isArticle + }, + /** + * 初始化 Pjax + * @param cb 除文章外的其他页面加载回调 + */ + initPjax: function (cb) { + if ($('#pjax').length === 1) { + $.pjax({ + selector: 'a', + container: '#pjax', + show: '', + cache: false, + storage: true, + titleSuffix: '', + filter: function (href, element) { + if (element.getAttribute('target') === '_blank') { + return true + } + if (href === Label.servePath + '/rss.xml' || + href.indexOf(Label.servePath + '/admin-index.do') > -1) { + return true + } + if (href.indexOf(Label.servePath) > -1) { + return false + } + return true + }, + callback: function () { + Util.parseMarkdown() + Util.parseLanguage() + cb && cb() + }, + }) + NProgress.configure({showSpinner: false}) + $('#pjax').bind('pjax.start', function () { + NProgress.start() + }) + $('#pjax').bind('pjax.end', function () { + window.scroll(window.scrollX, 0) + NProgress.done() + }) + } + }, + /** + * 图片预览 + */ + previewImg: function () { + $('body').on('click', '.vditor-reset img', function () { + if ($(this).hasClass('prevent')) { + return + } + window.open(this.src) + }) + }, + /** + * 异步添加 css + * @param url css 文件访问地址 + * @param id css 文件标示 + */ + addStyle: function (url, id) { + if (!document.getElementById(id)) { + var styleElement = document.createElement('link') + styleElement.id = id + styleElement.setAttribute('rel', 'stylesheet') + styleElement.setAttribute('type', 'text/css') + styleElement.setAttribute('href', url) + document.getElementsByTagName('head')[0].appendChild(styleElement) + } + }, + /** + * 异步添加 js + * @param url js 文件访问地址 + * @param id js 文件标示 + */ + addScript: function (url, id) { + if (!document.getElementById(id)) { + var xhrObj = new XMLHttpRequest() + xhrObj.open('GET', url, false) + xhrObj.setRequestHeader('Accept', + 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01') + xhrObj.send('') + var scriptElement = document.createElement('script') + scriptElement.id = id + scriptElement.type = 'text/javascript' + scriptElement.text = xhrObj.responseText + document.getElementsByTagName('head')[0].appendChild(scriptElement) + } + }, + /* + * @description 解析语法高亮 + */ + parseLanguage: function () { + Vditor.highlightRender(Label.hljsStyle, !Label.luteAvailable, document) + }, + /** + * 按需加载数学公式、流程图、代码复制、五线谱、多媒体、图表 + * @returns {undefined} + */ + parseMarkdown: function () { + + if (typeof Vditor === 'undefined') { + Util.addScript( + 'https://cdn.jsdelivr.net/npm/vditor@1.8.9/dist/method.min.js', + 'vditorPreviewScript') + } + + Vditor.codeRender(document.body, Label.langLabel) + if (Label.luteAvailable) { + Vditor.mathRenderByLute(document.body) + } else { + Vditor.mathRender(document.body) + } + + Vditor.abcRender() + Vditor.chartRender() + Vditor.mediaRender(document.body) + Vditor.mermaidRender(document.body) + }, + /** + * @description IE6/7,跳转到 kill-browser 页面 + */ + killIE: function (ieVersion) { + var addKillPanel = function () { + if (Cookie.readCookie('showKill') === '') { + try { + var left = ($(window).width() - 781) / 2, + top1 = ($(window).height() - 680) / 2 + var killIEHTML = '
    ' + + '' + $('body').append(killIEHTML) + } catch (e) { + var left = 10, + top1 = 0 + var killIEHTML = '
    ' + + '' + document.body.innerHTML = document.body.innerHTML + killIEHTML + } + } + } + + var ua = navigator.userAgent.split('MSIE')[1] + if (ua) { + if (!ieVersion) { + ieVersion = 7 + } + if (parseFloat(ua.split(';')) <= ieVersion) { + addKillPanel() + } + } + }, + /** + * @description 切换到手机版 + * @param {String} skin 切换前的皮肤名称 + */ + switchMobile: function (skin) { + Cookie.createCookie('btouch_switch_toggle', skin, 365) + setTimeout(function () { + location.reload() + }, 1250) + }, + /** + * @description topbar 相关事件 + */ + setTopBar: function () { + var $top = $('#top') + if ($top.length === 1) { + var $showTop = $('#showTop') + $showTop.click(function () { + $top.slideDown() + $showTop.hide() + }) + $('#hideTop').click(function () { + $top.slideUp() + $showTop.show() + }) + } + }, + /** + * @description 回到顶部 + */ + goTop: function () { + $('html, body').animate({scrollTop: 0}, 800) + }, + /** + * @description 回到底部 + */ + goBottom: function (bottom) { + if (!bottom) { + bottom = 0 + } + $('html, body'). + animate({scrollTop: $(document).height() - $(window).height() - bottom}, + 800) + }, + /** + * @description 页面初始化执行的函数 + */ + init: function () { + Util.killIE() + Util.parseMarkdown() + Util.parseLanguage() + Util.initSW() + Util.previewImg() + Util.initDebugInfo() + }, + /** + * 调试区域文案 + */ + initDebugInfo: function () { + console.log( + '%cSolo%c\n 🎸一款小而美的博客系统,专为程序员设计。' + Label.version + ' © ' + + (new Date).getFullYear(), + 'font-size:96px;color:#3b3e43', 'font-size:12px;color:rgba(0,0,0,0.38);') + }, + /** + * @description 注册 Service Work + */ + initSW: function () { + if (navigator.serviceWorker) { + navigator.serviceWorker.register('/sw.js', {scope: '/'}) + } + }, + /** + * @description 替换侧边栏表情为图片 + * @param {Dom} comments 评论内容元素 + */ + replaceSideEm: function (comments) { + for (var i = 0; i < comments.length; i++) { + var $comment = $(comments[i]) + $comment.html($comment.html()) + } + }, + /** + * @description 根据 tags,穿件云效果 + * @param {String} [id] tags 根元素 id,默认为 tags + */ + buildTags: function (id) { + id = id || 'tags' + // 根据引用次数添加样式,产生云效果 + var classes = ['tags1', 'tags2', 'tags3', 'tags4', 'tags5'], + bList = $('#' + id + ' b').get() + var max = parseInt($('#' + id + ' b').last().text()) + var distance = Math.ceil(max / classes.length) + for (var i = 0; i < bList.length; i++) { + var num = parseInt(bList[i].innerHTML) + // 算出当前 tag 数目所在的区间,加上 class + for (var j = 0; j < classes.length; j++) { + if (num > j * distance && num <= (j + 1) * distance) { + bList[i].parentNode.className = classes[j] + break + } + } + } + + // 按字母或者中文拼音进行排序 + $('#' + id).html($('#' + id + ' li').get().sort(function (a, b) { + var valA = $(a).find('span').text().toLowerCase() + var valB = $(b).find('span').text().toLowerCase() + // 对中英文排序的处理 + return valA.localeCompare(valB) + })) + }, + /** + * @description 时间戳转化为时间格式 + * @param {String} time 时间 + * @param {String} format 格式化后日期格式 + * @returns {String} 格式化后的时间 + */ + toDate: function (time, format) { + var dateTime = new Date(time) + var o = { + 'M+': dateTime.getMonth() + 1, //month + 'd+': dateTime.getDate(), //day + 'H+': dateTime.getHours(), //hour + 'm+': dateTime.getMinutes(), //minute + 's+': dateTime.getSeconds(), //second + 'q+': Math.floor((dateTime.getMonth() + 3) / 3), //quarter + 'S': dateTime.getMilliseconds(), //millisecond + } + + if (/(y+)/.test(format)) { + format = format.replace(RegExp.$1, + (dateTime.getFullYear() + '').substr(4 - RegExp.$1.length)) + } + + for (var k in o) { + if (new RegExp('(' + k + ')').test(format)) { + format = format.replace(RegExp.$1, + RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr( + ('' + o[k]).length)) + } + } + return format + }, +} +if (!Cookie) { + /** + * @description Cookie 相关操作 + * @static + */ + var Cookie = { + /** + * @description 读取 cookie + * @param {String} name cookie key + * @returns {String} 对应 key 的值,如 key 不存在则返回 "" + */ + readCookie: function (name) { + var nameEQ = name + '=' + var ca = document.cookie.split(';') + for (var i = 0; i < ca.length; i++) { + var c = ca[i] + while (c.charAt(0) == ' ') + c = c.substring(1, c.length) + if (c.indexOf(nameEQ) == 0) + return decodeURIComponent(c.substring(nameEQ.length, c.length)) + } + return '' + }, + /** + * @description 清除 Cookie + * @param {String} name 清除 key 为 name 的该条 Cookie + */ + eraseCookie: function (name) { + this.createCookie(name, '', -1) + }, + /** + * @description 创建 Cookie + * @param {String} name 每条 Cookie 唯一的 key + * @param {String} value 每条 Cookie 对应的值,将被 UTF-8 编码 + * @param {Int} days Cookie 保存时间 + */ + createCookie: function (name, value, days) { + var expires = '' + if (days) { + var date = new Date() + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)) + expires = '; expires=' + date.toGMTString() + } + document.cookie = name + '=' + encodeURIComponent(value) + expires + + '; path=/' + }, + } +} diff --git a/src/main/webapp/js/common.min.js b/src/main/webapp/js/common.min.js new file mode 100644 index 00000000..d4b2392f --- /dev/null +++ b/src/main/webapp/js/common.min.js @@ -0,0 +1 @@ +var Util={isArticlePage:function(e){var t=!0;return-1";$("body").append(t)}catch(e){t="
    ",document.body.innerHTML=document.body.innerHTML+t}}())},switchMobile:function(e){Cookie.createCookie("btouch_switch_toggle",e,365),setTimeout(function(){location.reload()},1250)},setTopBar:function(){var e=$("#top");if(1===e.length){var t=$("#showTop");t.click(function(){e.slideDown(),t.hide()}),$("#hideTop").click(function(){e.slideUp(),t.show()})}},goTop:function(){$("html, body").animate({scrollTop:0},800)},goBottom:function(e){e||(e=0),$("html, body").animate({scrollTop:$(document).height()-$(window).height()-e},800)},init:function(){Util.killIE(),Util.parseMarkdown(),Util.parseLanguage(),Util.initSW(),Util.previewImg(),Util.initDebugInfo()},initDebugInfo:function(){console.log("%cSolo%c\n 🎸一款小而美的博客系统,专为程序员设计。"+Label.version+" © "+(new Date).getFullYear(),"font-size:96px;color:#3b3e43","font-size:12px;color:rgba(0,0,0,0.38);")},initSW:function(){navigator.serviceWorker&&navigator.serviceWorker.register("/sw.js",{scope:"/"})},replaceSideEm:function(e){for(var t=0;t>10|55296,1023&r|56320)}function i(){T()}var e,h,b,o,a,m,f,g,w,l,u,T,C,s,k,v,c,p,y,E="sizzle"+1*new Date,x=n.document,N=0,r=0,S=ie(),A=ie(),j=ie(),D=function(e,t){return e===t&&(u=!0),0},L={}.hasOwnProperty,t=[],H=t.pop,q=t.push,_=t.push,M=t.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+R+")"+R+"*"),U=new RegExp("="+R+"*([^\\]'\"]*?)"+R+"*\\]","g"),V=new RegExp(W),Q=new RegExp("^"+B+"$"),Y={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),bool:new RegExp("^(?:"+F+")$","i"),needsContext:new RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},J=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=/'|\\/g,ne=new RegExp("\\\\([\\da-f]{1,6}"+R+"?|("+R+")|.)","ig");try{_.apply(t=M.call(x.childNodes),x.childNodes),t[x.childNodes.length].nodeType}catch(e){_={apply:t.length?function(e,t){q.apply(e,M.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function re(e,t,n,r){var i,o,a,s,l,u,c,d,f=t&&t.ownerDocument,p=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==p&&9!==p&&11!==p)return n;if(!r&&((t?t.ownerDocument||t:x)!==C&&T(t),t=t||C,k)){if(11!==p&&(u=Z.exec(e)))if(i=u[1]){if(9===p){if(!(a=t.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(t,a)&&a.id===i)return n.push(a),n}else{if(u[2])return _.apply(n,t.getElementsByTagName(e)),n;if((i=u[3])&&h.getElementsByClassName&&t.getElementsByClassName)return _.apply(n,t.getElementsByClassName(i)),n}if(h.qsa&&!j[e+" "]&&(!v||!v.test(e))){if(1!==p)f=t,d=e;else if("object"!==t.nodeName.toLowerCase()){for((s=t.getAttribute("id"))?s=s.replace(te,"\\$&"):t.setAttribute("id",s=E),o=(c=m(e)).length,l=Q.test(s)?"#"+s:"[id='"+s+"']";o--;)c[o]=l+" "+he(c[o]);d=c.join(","),f=ee.test(e)&&fe(t.parentNode)||t}if(d)try{return _.apply(n,f.querySelectorAll(d)),n}catch(e){}finally{s===E&&t.removeAttribute("id")}}}return g(e.replace($,"$1"),t,n,r)}function ie(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function oe(e){return e[E]=!0,e}function ae(e){var t=C.createElement("div");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function se(e,t){for(var n=e.split("|"),r=n.length;r--;)b.attrHandle[n[r]]=t}function le(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function ue(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function ce(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function de(a){return oe(function(o){return o=+o,oe(function(e,t){for(var n,r=a([],e.length,o),i=r.length;i--;)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function fe(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in h=re.support={},a=re.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},T=re.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:x;return r!==C&&9===r.nodeType&&r.documentElement&&(s=(C=r).documentElement,k=!a(C),(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",i,!1):n.attachEvent&&n.attachEvent("onunload",i)),h.attributes=ae(function(e){return e.className="i",!e.getAttribute("className")}),h.getElementsByTagName=ae(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),h.getElementsByClassName=K.test(C.getElementsByClassName),h.getById=ae(function(e){return s.appendChild(e).id=E,!C.getElementsByName||!C.getElementsByName(E).length}),h.getById?(b.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n=t.getElementById(e);return n?[n]:[]}},b.filter.ID=function(e){var t=e.replace(ne,d);return function(e){return e.getAttribute("id")===t}}):(delete b.find.ID,b.filter.ID=function(e){var n=e.replace(ne,d);return function(e){var t=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}}),b.find.TAG=h.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):h.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"!==e)return o;for(;n=o[i++];)1===n.nodeType&&r.push(n);return r},b.find.CLASS=h.getElementsByClassName&&function(e,t){return void 0!==t.getElementsByClassName&&k?t.getElementsByClassName(e):void 0},c=[],v=[],(h.qsa=K.test(C.querySelectorAll))&&(ae(function(e){s.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+R+"*(?:value|"+F+")"),e.querySelectorAll("[id~="+E+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+E+"+*").length||v.push(".#.+[+~]")}),ae(function(e){var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+R+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(h.matchesSelector=K.test(p=s.matches||s.webkitMatchesSelector||s.mozMatchesSelector||s.oMatchesSelector||s.msMatchesSelector))&&ae(function(e){h.disconnectedMatch=p.call(e,"div"),p.call(e,"[s!='']:x"),c.push("!=",W)}),v=v.length&&new RegExp(v.join("|")),c=c.length&&new RegExp(c.join("|")),t=K.test(s.compareDocumentPosition),y=t||K.test(s.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return u=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!h.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===x&&y(x,e)?-1:t===C||t.ownerDocument===x&&y(x,t)?1:l?O(l,e)-O(l,t):0:4&n?-1:1)}:function(e,t){if(e===t)return u=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:l?O(l,e)-O(l,t):0;if(i===o)return le(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?le(a[r],s[r]):a[r]===x?-1:s[r]===x?1:0}),C},re.matches=function(e,t){return re(e,null,null,t)},re.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),t=t.replace(U,"='$1']"),h.matchesSelector&&k&&!j[t+" "]&&(!c||!c.test(t))&&(!v||!v.test(t)))try{var n=p.call(e,t);if(n||h.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(ne,d),e[3]=(e[3]||e[4]||e[5]||"").replace(ne,d),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||re.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&re.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=m(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(ne,d).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=S[e+" "];return t||(t=new RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&S(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=re.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1(?:<\/\1>|)$/,T=/^.[^:#\[\.,]*$/;function E(e,n,r){if(k.isFunction(n))return k.grep(e,function(e,t){return!!n.call(e,t,e)!==r});if(n.nodeType)return k.grep(e,function(e){return e===n!==r});if("string"==typeof n){if(T.test(n))return k.filter(n,e,r);n=k.filter(n,e)}return k.grep(e,function(e){return-1)[^>]*|#([\w-]*))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||N,"string"!=typeof e)return e.nodeType?(this.context=this[0]=e,this.length=1,this):k.isFunction(e)?void 0!==n.ready?n.ready(e):e(k):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),k.makeArray(e,this));if(!(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:h,!0)),w.test(r[1])&&k.isPlainObject(t))for(r in t)k.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}if((i=h.getElementById(r[2]))&&i.parentNode){if(i.id!==r[2])return N.find(e);this.length=1,this[0]=i}return this.context=h,this.selector=e,this}).prototype=k.fn,N=k(h);var A=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}k.fn.extend({has:function(e){var t,n=k(e,this),r=n.length;return this.filter(function(){for(t=0;t
    a",v.leadingWhitespace=3===Y.firstChild.nodeType,v.tbody=!Y.getElementsByTagName("tbody").length,v.htmlSerialize=!!Y.getElementsByTagName("link").length,v.html5Clone="<:nav>"!==h.createElement("nav").cloneNode(!0).outerHTML,G.type="checkbox",G.checked=!0,J.appendChild(G),v.appendChecked=G.checked,Y.innerHTML="",v.noCloneChecked=!!Y.cloneNode(!0).lastChild.defaultValue,J.appendChild(Y),(G=h.createElement("input")).setAttribute("type","radio"),G.setAttribute("checked","checked"),G.setAttribute("name","t"),Y.appendChild(G),v.checkClone=Y.cloneNode(!0).cloneNode(!0).lastChild.checked,v.noCloneEvent=!!Y.addEventListener,Y[k.expando]=1,v.attributes=!Y.getAttribute(k.expando);var oe={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:v.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};function ae(e,t){var n,r,i=0,o=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||k.nodeName(r,t)?o.push(r):k.merge(o,ae(r,t));return void 0===t||t&&k.nodeName(e,t)?k.merge([e],o):o}function se(e,t){for(var n,r=0;null!=(n=e[r]);r++)k._data(n,"globalEval",!t||k._data(t[r],"globalEval"))}oe.optgroup=oe.option,oe.tbody=oe.tfoot=oe.colgroup=oe.caption=oe.thead,oe.th=oe.td;var le=/<|&#?\w+;/,ue=/"!==d[1]||ue.test(a)?0:l:l.firstChild)&&a.childNodes.length;o--;)k.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(k.merge(h,l.childNodes),l.textContent="";l.firstChild;)l.removeChild(l.firstChild);l=p.lastChild}else h.push(t.createTextNode(a));for(l&&p.removeChild(l),v.appendChecked||k.grep(ae(h,"input"),ce),m=0;a=h[m++];)if(r&&-1]","i"),Ce=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ke=/\s*$/g,Ae=ie(h).appendChild(h.createElement("div"));function je(e,t){return k.nodeName(e,"table")&&k.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function De(e){return e.type=(null!==k.find.attr(e,"type"))+"/"+e.type,e}function Le(e){var t=Ne.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function He(e,t){if(1===t.nodeType&&k.hasData(e)){var n,r,i,o=k._data(e),a=k._data(t,o),s=o.events;if(s)for(n in delete a.handle,a.events={},s)for(r=0,i=s[n].length;r")},clone:function(e,t,n){var r,i,o,a,s,l=k.contains(e.ownerDocument,e);if(v.html5Clone||k.isXMLDoc(e)||!Te.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Ae.innerHTML=e.outerHTML,Ae.removeChild(o=Ae.firstChild)),!(v.noCloneEvent&&v.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(r=ae(o),s=ae(e),a=0;null!=(i=s[a]);++a)r[a]&&qe(i,r[a]);if(t)if(n)for(s=s||ae(e),r=r||ae(o),a=0;null!=(i=s[a]);a++)He(i,r[a]);else He(e,o);return 0<(r=ae(o,"script")).length&&se(r,!l&&ae(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var n,r,i,o,a=0,s=k.expando,l=k.cache,u=v.attributes,c=k.event.special;null!=(n=e[a]);a++)if((t||O(n))&&(o=(i=n[s])&&l[i])){if(o.events)for(r in o.events)c[r]?k.event.remove(n,r):k.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u||void 0===n.removeAttribute?n[s]=void 0:n.removeAttribute(s),d.push(i))}}}),k.fn.extend({domManip:_e,detach:function(e){return Me(this,e,!0)},remove:function(e){return Me(this,e)},text:function(e){return K(this,function(e){return void 0===e?k.text(this):this.empty().append((this[0]&&this[0].ownerDocument||h).createTextNode(e))},null,e,arguments.length)},append:function(){return _e(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||je(this,e).appendChild(e)})},prepend:function(){return _e(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=je(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return _e(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return _e(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&k.cleanData(ae(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&k.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return k.clone(this,e,t)})},html:function(e){return K(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(we,""):void 0;if("string"==typeof e&&!ke.test(e)&&(v.htmlSerialize||!Te.test(e))&&(v.leadingWhitespace||!ne.test(e))&&!oe[(ee.exec(e)||["",""])[1].toLowerCase()]){e=k.htmlPrefilter(e);try{for(;n")).appendTo(t.documentElement))[0].contentWindow||Oe[0].contentDocument).document).write(),t.close(),n=Re(e,t),Oe.detach()),Fe[e]=n),n}function Pe(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];for(o in i=n.apply(e,r||[]),t)e.style[o]=a[o];return i}var We=/^margin/,Ie=new RegExp("^("+X+")(?!px)[a-z%]+$","i"),$e=h.documentElement;!function(){var r,i,o,a,s,l,u=h.createElement("div"),c=h.createElement("div");if(c.style){function e(){var e,t,n=h.documentElement;n.appendChild(u),c.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",r=o=l=!1,i=s=!0,C.getComputedStyle&&(t=C.getComputedStyle(c),r="1%"!==(t||{}).top,l="2px"===(t||{}).marginLeft,o="4px"===(t||{width:"4px"}).width,c.style.marginRight="50%",i="4px"===(t||{marginRight:"4px"}).marginRight,(e=c.appendChild(h.createElement("div"))).style.cssText=c.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",e.style.marginRight=e.style.width="0",c.style.width="1px",s=!parseFloat((C.getComputedStyle(e)||{}).marginRight),c.removeChild(e)),c.style.display="none",(a=0===c.getClientRects().length)&&(c.style.display="",c.innerHTML="
    t
    ",c.childNodes[0].style.borderCollapse="separate",(e=c.getElementsByTagName("td"))[0].style.cssText="margin:0;border:0;padding:0;display:none",(a=0===e[0].offsetHeight)&&(e[0].style.display="",e[1].style.display="none",a=0===e[0].offsetHeight)),n.removeChild(u)}c.style.cssText="float:left;opacity:.5",v.opacity="0.5"===c.style.opacity,v.cssFloat=!!c.style.cssFloat,c.style.backgroundClip="content-box",c.cloneNode(!0).style.backgroundClip="",v.clearCloneStyle="content-box"===c.style.backgroundClip,(u=h.createElement("div")).style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",c.innerHTML="",u.appendChild(c),v.boxSizing=""===c.style.boxSizing||""===c.style.MozBoxSizing||""===c.style.WebkitBoxSizing,k.extend(v,{reliableHiddenOffsets:function(){return null==r&&e(),a},boxSizingReliable:function(){return null==r&&e(),o},pixelMarginRight:function(){return null==r&&e(),i},pixelPosition:function(){return null==r&&e(),r},reliableMarginRight:function(){return null==r&&e(),s},reliableMarginLeft:function(){return null==r&&e(),l}})}}();var ze,Xe,Ue=/^(top|right|bottom|left)$/;function Ve(e,t){return{get:function(){return e()?void delete this.get:(this.get=t).apply(this,arguments)}}}C.getComputedStyle?(ze=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},Xe=function(e,t,n){var r,i,o,a,s=e.style;return""!==(a=(n=n||ze(e))?n.getPropertyValue(t)||n[t]:void 0)&&void 0!==a||k.contains(e.ownerDocument,e)||(a=k.style(e,t)),n&&!v.pixelMarginRight()&&Ie.test(a)&&We.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o),void 0===a?a:a+""}):$e.currentStyle&&(ze=function(e){return e.currentStyle},Xe=function(e,t,n){var r,i,o,a,s=e.style;return null==(a=(n=n||ze(e))?n[t]:void 0)&&s&&s[t]&&(a=s[t]),Ie.test(a)&&!Ue.test(t)&&(r=s.left,(o=(i=e.runtimeStyle)&&i.left)&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"});var Qe=/alpha\([^)]*\)/i,Ye=/opacity\s*=\s*([^)]*)/i,Je=/^(none|table(?!-c[ea]).+)/,Ge=new RegExp("^("+X+")(.*)$","i"),Ke={position:"absolute",visibility:"hidden",display:"block"},Ze={letterSpacing:"0",fontWeight:"400"},et=["Webkit","O","Moz","ms"],tt=h.createElement("div").style;function nt(e){if(e in tt)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=et.length;n--;)if((e=et[n]+t)in tt)return e}function rt(e,t){for(var n,r,i,o=[],a=0,s=e.length;a
    a",ct=ft.getElementsByTagName("a")[0],dt.setAttribute("type","checkbox"),ft.appendChild(dt),(ct=ft.getElementsByTagName("a")[0]).style.cssText="top:1px",v.getSetAttribute="t"!==ft.className,v.style=/top/.test(ct.getAttribute("style")),v.hrefNormalized="/a"===ct.getAttribute("href"),v.checkOn=!!dt.value,v.optSelected=ht.selected,v.enctype=!!h.createElement("form").enctype,pt.disabled=!0,v.optDisabled=!ht.disabled,(dt=h.createElement("input")).setAttribute("value",""),v.input=""===dt.getAttribute("value"),dt.value="t",dt.setAttribute("type","radio"),v.radioValue="t"===dt.value;var wt=/\r/g,Tt=/[\x20\t\r\n\f]+/g;k.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=k.isFunction(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,k(this).val()):n)?t="":"number"==typeof t?t+="":k.isArray(t)&&(t=k.map(t,function(e){return null==e?"":e+""})),(r=k.valHooks[this.type]||k.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=k.valHooks[t.type]||k.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(wt,""):null==e?"":e:void 0}}),k.extend({valHooks:{option:{get:function(e){var t=k.find.attr(e,"value");return null!=t?t:k.trim(k.text(e)).replace(Tt," ")}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||i<0,a=o?null:[],s=o?i+1:r.length,l=i<0?s:o?i:0;l").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.filters.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u=k.css(e,"position"),c=k(e),d={};"static"===u&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),l=k.css(e,"left"),i=("absolute"===u||"fixed"===u)&&-1"),this._build(t)},_build:function(t){var e=this._getInst(t),a=e.id,s=e.settings,i=this._getDefaults(this._defaults,s,"styleClass"),n=s.pageCount,l=s.currentPage,o="<$tagStart class='button paginate-page'> "+s.previousPageText+" ",d="<$tagStart class='button paginate-page'> "+s.nextPageText+" ",r="",c=""+l+"/"+n+"",u="
    "+s.goText+" ";switch(o=1===l||0===l?o.replace("$tagStart","div").replace("$tagEnd","div"):o.replace("$tagStart","a href='javascript:void(0)'").replace("$tagEnd","a"),d=l!==n?d.replace("$tagStart","a href='javascript:void(0)'").replace("$tagEnd","a"):d.replace("$tagStart","div").replace("$tagEnd","div"),s.type){case"taobao":var g=this._getTaobaoPages(l,n),h="";u="";for(var p=0;p"+g[p].text+"";break;case"custom":for(g=s.custom,h="",p=0;p"+g[p]+""}1===n?f("#"+a+"Paginate").html(c):f("#"+a+"Paginate").html(o+r+d+c+u),this._bindEvent(t)},_getTaobaoPages:function(t,e){var a=[],s=2,i=3,n=0,l=0,o=0;for(4<=t?(i=t-Math.ceil(2),n=(l=t+Math.ceil(2)+1)"),this._build(t)},_build:function(t){var e=this._getInst(t),a=e.settings,s=e.id,i=this._getDefaults(this._defaults,a,"styleClass"),n="
    ";y("#"+s+"Table").html(n),this._buildHeader(t),a.data&&(this._buildBody(t),this._bindEvent(t))},_buildHeader:function(t){for(var e=this._getInst(t).settings.colModel,a=this._getInst(t).id,s="",i=0;i":s+="":s+=""+e[i].text?e[i].text:""}y("#"+a+"TableHeader").html(s+"
    ","checkbox"===e[i].type?e[i].isLabel?s+=e[i].text:s+="
    ")},_buildBody:function(t){for(var e=this._getInst(t).id,a=this._getInst(t).settings,s=a.data,i=this._getDefaults(this._defaults,a,"styleClass"),n="",l=0;l
    "+s[l].groupName+"
    "),n+=""+this._buildData(t,s[l].groupData,l)+"
    ";y("#"+e+"TableMain").html(n)},_buildData:function(t,e,a){for(var s=this._getInst(t).id,i=this._getInst(t).settings,n=i.colModel,l=this._getDefaults(this._defaults,i,"styleClass"),o="",d=0;d",u="",g=l.oddRowClass;d%2==1&&(g=l.evenRowClass),i.expendRow&&(u=''),r.uuuid=i.length++;for(var h=0;h",n[h].type){var b="",_="",m=s+"_"+p+"_"+a+"_"+d;r[p].value&&(b="checked='checked'",c=c.replace("$CLASS",l.lineSelectedClass+" "+g),i.selectedRows.push(r)),r[p].disabled&&(_="disabled='disabled'"),f=""}n[h].style&&(f="
    "+f+"
    "),u+=f+""}if(o+=c.replace("$CLASS",g)+u+"",i.expendRow){var C=r[i.expendRow.index];void 0===C&&(C=""),o+=""+C+""}o+=""}return 0===e.length&&(o=""+i.noDataTip+""),o},_bindEvent:function(t){for(var e=this._getInst(t),a=e.id,s=e.settings,i=e.settings.colModel,n=this._getDefaults(this._defaults,s,"styleClass"),l=s.data,o=0;o
    "+s.title+"
    ";s.hideFooter||(r=""+s.okText+""+s.cancelText+"");var u="
    "+c+"
    "+r+"
    ",g="";s.modal&&0===_("."+l.background).length&&(g="
    ");_("#"+a).wrap("
    ");var h=_(t).clone(!0);_(t).remove(),_("body").append(g+u),_(_("#"+a+"Dialog ."+l.main+" div").get(0)).append(h),_(h).show();var p="",f="",v=_("#"+a+"Dialog");f=s.position?(p=s.position.top,s.position.left):(p=parseInt((i-o)/2),parseInt((n-d)/2)),v.css({top:p+"px",left:f+"px"}),_("#"+a+"Dialog ."+l.closeIcon).bind("click",function(){_.dialog._close(a,s)});var b=_("#"+a+"Dialog ."+l.footer+" a");_(b.get(1)).bind("click",function(){_.dialog._close(a,s)}),_(b.get(0)).bind("click",function(){(void 0===s.ok||s.ok())&&_.dialog._close(a,s)}),this._bindMove(a,l.headerBg,o,d),_(window).keyup(function(t){27===t.keyCode&&_.dialog._close(a,s)})},_bindMove:function(a,t){_("#"+a+"Dialog ."+t).mousedown(function(t){var e=document;t||(t=window.event);var s=document.getElementById(a+"Dialog"),i=t.clientX-parseInt(s.style.left),n=t.clientY-parseInt(s.style.top);e.ondragstart="return false;",e.onselectstart="return false;",e.onselect="document.selection.empty();",this.setCapture?this.setCapture():window.captureEvents&&window.captureEvents(Event.MOUSEMOVE|Event.MOUSEUP),e.onmousemove=function(t){t||(t=window.event);var e=t.clientX-i,a=t.clientY-n;e<0&&(e=0),e>_(window).width()-_(s).width()&&(e=_(window).width()-_(s).width()),a<0&&(a=0),a>_(window).height()-_(s).height()&&(a=_(window).height()-_(s).height()),s.style.left=e+"px",s.style.top=a+"px"},e.onmouseup=function(){this.releaseCapture?this.releaseCapture():window.captureEvents&&window.captureEvents(Event.MOUSEMOVE|Event.MOUSEUP),e.onmousemove=null,e.onmouseup=null,e.ondragstart=null,e.onselectstart=null,e.onselect=null}})},_close:function(t,e){if("none"!==_("#"+t+"Dialog").css("display")&&(void 0===e.close||e.close())&&(_("#"+t+"Dialog").hide(),e.modal)){var a=this._getDefaults(_.dialog._defaults,e,"styleClass");_("."+a.background).hide()}},_closeDialog:function(t){var e=this._getInst(t),a=e.id,s=e.settings;_.dialog._close(a,s)},_openDialog:function(t){var e=this._getInst(t),a=e.id,s=e.settings;if(_("#"+a+"Dialog").show(),s.modal){var i=this._getDefaults(_.dialog._defaults,s,"styleClass");_("."+i.background).show()}},_updateDialog:function(t,e){var a=this._getInst(t),s=a.id,i=a.settings,n=this._getDefaults(_.dialog._defaults,i,"styleClass");_.extend(i,e);var l=_("#"+s+"Dialog");e.position&&l.css({top:e.position.top,left:e.position.left}),e.width&&(l.width(e.width+26),l.find("."+n.main+" div")[0].style.width=e.width+"px",l.find("."+n.headerBg).width(e.width+18)),e.height&&(l.find("."+n.main+" div")[0].style.height=e.height+"px"),e.title&&l.find("."+n.title).html(e.title),void 0!==e.modal&&(e.modal?_("."+n.background).show():_("."+n.background).hide()),void 0!==e.hideFooter&&(e.hideFooter?l.find("."+n.footer).hide():l.find("."+n.footer).show())},_getDefaults:function(t,e,a){if("styleClass"===a){if("default"===e.theme||void 0===e.theme)return t.styleClass;for(var s in e.styleClass={},t[a])e.styleClass[s]=e.theme+"-"+t.styleClass[s]}else{if("height"===a||"width"===a)return null===e[a]||void 0===e[a]?"auto":e[a]+"px";if(null===e[a]||void 0===e[a])return t[a]}return e[a]}}),_.fn.dialog=function(t){var e=Array.prototype.slice.call(arguments);return"string"==typeof t?(e.shift(),_.dialog["_"+t+"Dialog"].apply(_.dialog,[this[0]].concat(e))):this.each(function(){_.dialog._attach(this,t)})},_.dialog=new t,window["DP_jQuery_"+e]=_}(jQuery),function(h){function t(){this._defaults={styleClass:{panelClass:"completed-panel",inputClass:"completed-input",ckClass:"completed-ck"},separator:","},this._settingsDataFormat={}}var e=(new Date).getTime(),s="completed";h.extend(t.prototype,{_attach:function(t,e){t.id||(this.uuid++,t.id="dp"+this.uuid);var a=this._newInst(h(t));a.settings=h.extend({buttonText:"\u9009\u62e9"},e||{}),h.data(t,s,a),this._init(t)},_newInst:function(t){return{id:t[0].id.replace(/([^A-Za-z0-9_])/g,"\\\\$1")}},_getInst:function(t){try{return h.data(t,s)}catch(t){throw"Missing instance data for this completed"}},_destroyCompleted:function(){},_init:function(t){var e=this._getInst(t),a=e.id,s=e.settings;this._buildHTML(a,s),h(document).click(function(t){t.target.id!==a&&h("#"+a+"SelectedPanel").hide()}),s.onlySelect||this._buildCheckboxPanel(a,s.data)},_buildHTML:function(t,e){var a=e.height+"px",s=this._getDefaults(h.completed._defaults,e,"styleClass"),i=h("#"+t),n="";e.onlySelect||(n+=""),n+="
    ","object"==typeof e.data&&e.data.sort(),i.after(n).bind("keyup",{settings:e},this._keyupAction).bind("keydown",function(t){e.chinese=t.keyCode,e.keydownVal=h(this).val()}).addClass(s.inputClass),e.tipNum=0},_keyupAction:function(t){var e=t.data.settings,a=h.completed._getCurrentWord(this,e);if(""===a.currentWord||27===t.keyCode||16===t.keyCode)return h("#"+this.id+"SelectedPanel").hide(),e.tipNum=0,void(e.afterKeyup&&e.afterKeyup(t));var s=h.completed._getMatchData(e.data,this.value,a.currentWord);if(38===t.keyCode&&(0=t.length&&(n.tipNum=0);for(var s="",l=0;l"+e+"");n.tipNum===l&&(o="class='selected'"),s+=""+d+""}a.html(s).show();var r=h("#"+i+"SelectedPanel a.selected");r.position().top+a.scrollTop()>50-r.height()&&a.scrollTop(r.position().top+a.scrollTop()+r.height()-50),r.position().top<0&&a.scrollTop(a.scrollTop-r.height()),h("#"+i+"SelectedPanel a").click(function(){var t=document.getElementById(i),e=h.completed._getCurrentWord(document.getElementById(i),n),a=h.completed._getMatchData(n.data,t.value,e.currentWord),s=t.value;t.value=s.substring(0,e.startPos)+a[n.tipNum]+s.substring(e.endPos,s.length),n.tipNum=0,h(t).focus(),n.afterSelected&&n.afterSelected(h(this))})}else a.html("").hide()},_buildCheckboxPanel:function(t,e){for(var a="",s=h("#"+t),i=0;i"+e[i]+"";h("#"+t+"CheckboxPanel").html(a+"
    "),h("#"+t+"CheckboxPanel").on("click","span",function(){var t=s.val(),e=this.innerHTML;if("selected"===this.className){this.className="";var a=t.substr(t.indexOf(e)+e.length,1);e===t||","!==a?s.val(t.replace(e,"")):s.val(t.replace(e+",",""))}else this.className="selected",""===t.replace(/\s/g,"")||","===t.substr(t.length-1,1)?s.val(t+e):s.val(t+","+e)}),this._matchChecked(t),s.blur(function(){h.completed._matchChecked(t)})},_matchChecked:function(t){var e=h("#"+t).val().split(",");h("#"+t+"CheckboxPanel span").removeClass().each(function(){for(var t=0;t"+e.text+"":""+e.text+"",r(e.target).before("
  • "+n+"
  • "),r("#"+i+"Panel").append("
    "+e.content+"
    ")},_removeTabs:function(t,e){for(var a=this._getInst(t),s=a.settings.data,i=a.id,n=0;n>10|55296,1023&r|56320)}function i(){T()}var e,d,b,o,a,h,p,g,w,u,l,T,C,s,E,m,c,v,y,k="sizzle"+1*new Date,x=n.document,S=0,r=0,N=ae(),D=ae(),j=ae(),A=function(e,t){return e===t&&(l=!0),0},q={}.hasOwnProperty,t=[],L=t.pop,H=t.push,F=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp($),G=new RegExp("^"+I+"$"),Y={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,re=function(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},ie=ye(function(e){return!0===e.disabled},{dir:"parentNode",next:"legend"});try{F.apply(t=O.call(x.childNodes),x.childNodes),t[x.childNodes.length].nodeType}catch(e){F={apply:t.length?function(e,t){H.apply(e,O.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function oe(e,t,n,r){var i,o,a,s,u,l,c,f=t&&t.ownerDocument,p=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==p&&9!==p&&11!==p)return n;if(!r&&((t?t.ownerDocument||t:x)!==C&&T(t),t=t||C,E)){if(11!==p&&(u=Z.exec(e)))if(i=u[1]){if(9===p){if(!(a=t.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(t,a)&&a.id===i)return n.push(a),n}else{if(u[2])return F.apply(n,t.getElementsByTagName(e)),n;if((i=u[3])&&d.getElementsByClassName&&t.getElementsByClassName)return F.apply(n,t.getElementsByClassName(i)),n}if(d.qsa&&!j[e+" "]&&(!m||!m.test(e))){if(1!==p)f=t,c=e;else if("object"!==t.nodeName.toLowerCase()){for((s=t.getAttribute("id"))?s=s.replace(ne,re):t.setAttribute("id",s=k),o=(l=h(e)).length;o--;)l[o]="#"+s+" "+ve(l[o]);c=l.join(","),f=ee.test(e)&&ge(t.parentNode)||t}if(c)try{return F.apply(n,f.querySelectorAll(c)),n}catch(e){}finally{s===k&&t.removeAttribute("id")}}}return g(e.replace(_,"$1"),t,n,r)}function ae(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function se(e){return e[k]=!0,e}function ue(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){for(var n=e.split("|"),r=n.length;r--;)b.attrHandle[n[r]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function fe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function pe(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function de(t){return function(e){return"label"in e&&e.disabled===t||"form"in e&&e.disabled===t||"form"in e&&!1===e.disabled&&(e.isDisabled===t||e.isDisabled!==!t&&("label"in e||!ie(e))!==t)}}function he(a){return se(function(o){return o=+o,se(function(e,t){for(var n,r=a([],e.length,o),i=r.length;i--;)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ge(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in d=oe.support={},a=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},T=oe.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:x;return r!==C&&9===r.nodeType&&r.documentElement&&(s=(C=r).documentElement,E=!a(C),x!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",i,!1):n.attachEvent&&n.attachEvent("onunload",i)),d.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ue(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ue(function(e){return s.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.find.ID=function(e,t){if(void 0!==t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}},b.filter.ID=function(e){var t=e.replace(te,f);return function(e){return e.getAttribute("id")===t}}):(delete b.find.ID,b.filter.ID=function(e){var n=e.replace(te,f);return function(e){var t=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}}),b.find.TAG=d.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"!==e)return o;for(;n=o[i++];)1===n.nodeType&&r.push(n);return r},b.find.CLASS=d.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&E)return t.getElementsByClassName(e)},c=[],m=[],(d.qsa=K.test(C.querySelectorAll))&&(ue(function(e){s.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||m.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),s.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(d.matchesSelector=K.test(v=s.matches||s.webkitMatchesSelector||s.mozMatchesSelector||s.oMatchesSelector||s.msMatchesSelector))&&ue(function(e){d.disconnectedMatch=v.call(e,"*"),v.call(e,"[s!='']:x"),c.push("!=",$)}),m=m.length&&new RegExp(m.join("|")),c=c.length&&new RegExp(c.join("|")),t=K.test(s.compareDocumentPosition),y=t||K.test(s.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},A=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===x&&y(x,e)?-1:t===C||t.ownerDocument===x&&y(x,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return ce(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?ce(a[r],s[r]):a[r]===x?-1:s[r]===x?1:0}),C},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),t=t.replace(U,"='$1']"),d.matchesSelector&&E&&!j[t+" "]&&(!c||!c.test(t))&&(!m||!m.test(t)))try{var n=v.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,f),e[3]=(e[3]||e[4]||e[5]||"").replace(te,f),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,f).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=oe.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function j(e,n,r){if(k.isFunction(n))return k.grep(e,function(e,t){return!!n.call(e,t,e)!==r});if(n.nodeType)return k.grep(e,function(e){return e===n!==r});if("string"==typeof n){if(D.test(n))return k.filter(n,e,r);n=k.filter(n,e)}return k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||A,"string"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):k.isFunction(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this);if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&k.isPlainObject(t))for(r in t)k.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}).prototype=k.fn,A=k(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function F(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]+)/i,ae=/^$|\/(?:java|ecma)script/i,se={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ue(e,t){var n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&k.nodeName(e,t)?k.merge([e],n):n}function le(e,t){for(var n=0,r=e.length;nx",m.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue;var he=E.documentElement,ge=/^key/,me=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ve=/^([^.]*)(?:\.(.+)|)/;function ye(){return!0}function xe(){return!1}function be(){try{return E.activeElement}catch(e){}}function we(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)we(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=xe;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,m=X.get(t);if(m)for(n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(he,i),n.guid||(n.guid=k.guid++),(u=m.events)||(u=m.events={}),(a=m.handle)||(a=m.handle=function(e){return void 0!==k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(O)||[""]).length;l--;)d=g=(s=ve.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,m=X.hasData(e)&&X.get(e);if(m&&(u=m.events)){for(l=(t=(t||"").match(O)||[""]).length;l--;)if(d=g=(s=ve.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){for(f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;o--;)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,m.handle)||k.removeEvent(e,d,m.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&X.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(X.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,Ce=/\s*$/g;function Ne(e,t){return k.nodeName(e,"table")&&k.nodeName(11!==t.nodeType?t:t.firstChild,"tr")&&e.getElementsByTagName("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function je(e){var t=ke.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Ae(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(X.hasData(e)&&(o=X.access(e),a=X.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=k.contains(e.ownerDocument,e);if(!(m.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ue(c),r=0,i=(o=ue(e)).length;r").prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Rt,Mt=[],It=/(=)\?(?=&|$)|\?\?/;function Wt(e){return k.isWindow(e)?e:9===e.nodeType&&e.defaultView}k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mt.pop()||k.expando+"_"+gt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(It.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&It.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=k.isFunction(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(It,"$1"+r):!1!==e.jsonp&&(e.url+=(mt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Mt.push(r)),o&&k.isFunction(i)&&i(o[0]),o=i=void 0}),"script"}),m.createHTMLDocument=((Rt=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Rt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(m.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=de([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),i=("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,r.left):(a=parseFloat(o)||0,parseFloat(u)||0),k.isFunction(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r,i,o=this[0];return o?o.getClientRects().length?(r=o.getBoundingClientRect()).width||r.height?(n=Wt(i=o.ownerDocument),e=i.documentElement,{top:r.top+n.pageYOffset-e.clientTop,left:r.left+n.pageXOffset-e.clientLeft}):r:{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n=this[0],r={top:0,left:0};return"fixed"===k.css(n,"position")?t=n.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),k.nodeName(e[0],"html")||(r=e.offset()),r={top:r.top+k.css(e[0],"borderTopWidth",!0),left:r.left+k.css(e[0],"borderLeftWidth",!0)}),{top:t.top-r.top-k.css(n,"marginTop",!0),left:t.left-r.left-k.css(n,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&"static"===k.css(e,"position");)e=e.offsetParent;return e||he})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r=Wt(e);return void 0===n?r?r[i]:e[t]:void(r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n)},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=Re(m.pixelPosition,function(e,t){if(t)return t=Pe(e,n),Fe.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return k.isWindow(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),k.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return k});var $t=C.jQuery,Bt=C.$;return k.noConflict=function(e){return C.$===k&&(C.$=Bt),e&&C.jQuery===k&&(C.jQuery=$t),k},e||(C.jQuery=C.$=k),k}); +!function(r){var i={support:{pjax:window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/(iPod|iPhone|iPad|WebApps\/.+CFNetwork)/),storage:!!window.localStorage},toInt:function(t){return parseInt(t)},stack:{},getTime:function(){return 1*new Date},getRealUrl:function(t){return t=(t=(t||"").replace(/\#.*?$/,"")).replace("?pjax=true&","?").replace("?pjax=true","").replace("&pjax=true","")},getUrlHash:function(t){return t.replace(/^[^\#]*(?:\#(.*?))?$/,"$1")},getLocalKey:function(t){var e="pjax_"+encodeURIComponent(t);return{data:e+"_data",time:e+"_time",title:e+"_title"}},removeAllCache:function(){if(i.support.storage)for(var t in localStorage)"pjax"===(t.split("_")||[""])[0]&&delete localStorage[t]},getCache:function(t,e,o){var a,n,l;if(e=i.toInt(e),t in i.stack){if(a=i.stack[t],ctime=i.getTime(),a.time+1e3*e>ctime)return a;delete i.stack[t]}else if(o&&i.support.storage){var r=i.getLocalKey(t);if(n=r.data,l=r.time,a=localStorage.getItem(n)){if(i.toInt(localStorage.getItem(l))+1e3*e>i.getTime())return{data:a,title:localStorage.getItem(r.title)};localStorage.removeItem(n),localStorage.removeItem(l),localStorage.removeItem(r.title)}}return null},setCache:function(t,e,o,a){var n,l=i.getTime();i.stack[t]={data:e,title:o,time:l},a&&i.support.storage&&(n=i.getLocalKey(t),localStorage.setItem(n.data,e),localStorage.setItem(n.time,l),localStorage.setItem(n.title,o))},removeCache:function(t){if(t=i.getRealUrl(t||location.href),delete i.stack[t],i.support.storage){var e=i.getLocalKey(t);localStorage.removeItem(e.data),localStorage.removeItem(e.time),localStorage.removeItem(e.title)}}},c=function(a){if(!(a=r.extend({selector:"",container:"",callback:function(){},filter:function(){}},a)).container||!a.selector)throw new Error("selector & container options must be set");r("body").delegate(a.selector,"click",function(t){if(1(.*?)<\/title>/);n&&(o=n[1])}o&&-1==o.indexOf(c.options.titleSuffix)&&(o+=c.options.titleSuffix),document.title=o,c.state={container:c.options.container,timeout:c.options.timeout,cache:c.options.cache,storage:c.options.storage,show:c.options.show,title:o,url:c.options.oldUrl};var l=r.param(c.options.data);""!=l&&(c.state.url=c.options.url+(/\?/.test(c.options.url)?"&":"?")+l),c.options.push?(c.active||(history.replaceState(r.extend({},c.state,{url:null}),document.title),c.active=!0),history.pushState(c.state,document.title,c.options.oldUrl)):!1===c.options.push&&history.replaceState(c.state,document.title,c.options.oldUrl),c.options.showFn&&c.options.showFn(t,function(){c.options.callback&&c.options.callback.call(c.options.element,{type:e?"cache":"success"})},e),c.options.cache&&!e&&i.setCache(c.options.url,t,o,c.options.storage)},c.request=function(a){a.hasOwnProperty("data")&&(c.defaultOptions.data=a.data),a=r.extend(!0,c.defaultOptions,a);var t,n=r(a.container);if(a.oldUrl=a.url,a.url=i.getRealUrl(a.url),r(a.element).length&&(t=i.toInt(r(a.element).attr("data-pjax-cache")))&&(a.cache=t),!0===a.cache&&(a.cache=86400),a.cache=i.toInt(a.cache),0===a.cache&&i.removeAllCache(),a.showFn||(a.showFn=function(t,e,o){c.showFn(a.show,n,t,e,o)}),c.options=a,c.options.success=c.success,a.cache&&(t=i.getCache(a.url,a.cache,a.storage)))return a.beforeSend(),a.title=t.title,c.success(t.data,!0),a.complete(),!0;c.xhr&&c.xhr.readyState<4&&(c.xhr.onreadystatechange=r.noop,c.xhr.abort()),c.xhr=r.ajax(c.options)};var n="state"in window.history,l=location.href;r(window).bind("popstate",function(t){var e=!n&&location.href==l;if(n=!0,!e){var o=t.state;if(o&&o.container)if(r(o.container).length){var a={url:o.url,container:o.container,push:null,timeout:o.timeout,cache:o.cache,storage:o.storage,title:o.title,element:null};c.request(a)}else window.location=location.href}}),i.support.pjax||((c=function(){return!0}).request=function(t){t&&t.url&&(location.href=t.url)}),r.pjax=c,r.pjax.util=i,"state"in r.Event.prototype||r.event.addProp("state")}(jQuery); +!function(n,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():n.NProgress=e()}(this,function(){var e,t,s={version:"0.2.0"},a=s.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
    '};function u(n,e,t){return n. + */ +!function(c){c.fn.qrcode=function(h){var r;function e(t){this.mode=r,this.data=t}function l(t,e){this.typeNumber=t,this.errorCorrectLevel=e,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function g(t,e){if(null==t.length)throw Error(t.length+"/"+e);for(var r=0;r>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++)o=!t&&1==(e>>r&1),this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o},setupTypeInfo:function(t,e){for(var r=d.getBCHTypeInfo(this.errorCorrectLevel<<3|e),o=0;o<15;o++){var n=!t&&1==(r>>o&1);o<6?this.modules[o][8]=n:o<8?this.modules[o+1][8]=n:this.modules[this.moduleCount-15+o][8]=n}for(o=0;o<15;o++)n=!t&&1==(r>>o&1),o<8?this.modules[8][this.moduleCount-o-1]=n:o<9?this.modules[8][15-o-1+1]=n:this.modules[8][15-o-1]=n;this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,s=this.moduleCount-1;0>>n&1)),d.getMask(e,o,s-u)&&(a=!a),this.modules[o][s-u]=a,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},l.PAD0=236,l.PAD1=17,l.createData=function(t,e,r){e=f.getRSBlocks(t,e);for(var o=new s,n=0;n8*t)throw Error("code length overflow. ("+o.getLengthInBits()+">"+8*t+")");for(o.getLengthInBits()+4<=8*t&&o.put(0,4);0!=o.getLengthInBits()%8;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*t)&&(o.put(l.PAD0,8),!(o.getLengthInBits()>=8*t));)o.put(l.PAD1,8);return l.createBytes(o,e)},l.createBytes=function(t,e){for(var r=0,o=0,n=0,i=Array(e.length),s=Array(e.length),u=0;u>>=1;return e},getPatternPosition:function(t){return d.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case 0:return 0==(e+r)%2;case 1:return 0==e%2;case 2:return 0==r%3;case 3:return 0==(e+r)%3;case 4:return 0==(Math.floor(e/2)+Math.floor(r/3))%2;case 5:return 0==e*r%2+e*r%3;case 6:return 0==(e*r%2+e*r%3)%2;case 7:return 0==(e*r%3+(e+r)%2)%2;default:throw Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new g([1],0),r=0;r>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}},"string"==typeof h&&(h={text:h}),h=c.extend({},{render:"canvas",width:256,height:256,typeNumber:-1,correctLevel:2,background:"#ffffff",foreground:"#000000"},h),this.each(function(){var t;if("canvas"==h.render){(t=new l(h.typeNumber,h.correctLevel)).addData(h.text),t.make();var e=document.createElement("canvas");e.width=h.width,e.height=h.height;for(var r=e.getContext("2d"),o=h.width/t.getModuleCount(),n=h.height/t.getModuleCount(),i=0;i").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background),r=h.width/t.getModuleCount(),o=h.height/t.getModuleCount(),n=0;n").css("height",o+"px").appendTo(e),s=0;s").css("width",r+"px").css("background-color",t.isDark(n,s)?h.foreground:h.background).appendTo(i);t=e,jQuery(t).appendTo(this)})}}(jQuery); \ No newline at end of file diff --git a/src/main/webapp/js/lib/jquery/jquery-3.1.0.min.js b/src/main/webapp/js/lib/jquery/jquery-3.1.0.min.js new file mode 100644 index 00000000..f6a6a99e --- /dev/null +++ b/src/main/webapp/js/lib/jquery/jquery-3.1.0.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.0 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.0",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null!=a?a<0?this[a+this.length]:this[a]:f.call(this)},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"label"in b&&b.disabled===a||"form"in b&&b.disabled===a||"form"in b&&b.disabled===!1&&(b.isDisabled===a||b.isDisabled!==!a&&("label"in b||!ea(b))!==a)}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(_,aa),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=V.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(_,aa),$.test(j[0].type)&&qa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&sa(j),!a)return G.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||$.test(a)&&qa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){if(r.isFunction(b))return r.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return r.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(C.test(b))return r.filter(b,a,c);b=r.filter(b,a)}return r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType})}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/\S+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0, +r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ja=/^$|\/(?:java|ecma)script/i,ka={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ka.optgroup=ka.option,ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead,ka.th=ka.td;function la(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function ma(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=la(l.appendChild(f),"script"),j&&ma(g),c){k=0;while(f=g[k++])ja.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var pa=d.documentElement,qa=/^key/,ra=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,sa=/^([^.]*)(?:\.(.+)|)/;function ta(){return!0}function ua(){return!1}function va(){try{return d.activeElement}catch(a){}}function wa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)wa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ua;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(pa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c-1:r.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h\x20\t\r\n\f]*)[^>]*)\/>/gi,ya=/\s*$/g;function Ca(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Da(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ea(a){var b=Aa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&za.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(m&&(e=oa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(la(e,"script"),Da),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=la(h),f=la(a),d=0,e=f.length;d0&&ma(g,!i&&la(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(la(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!ya.test(a)&&!ka[(ia.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Xa(a,b,c,d,e){return new Xa.prototype.init(a,b,c,d,e)}r.Tween=Xa,Xa.prototype={constructor:Xa,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Xa.propHooks[this.prop];return a&&a.get?a.get(this):Xa.propHooks._default.get(this)},run:function(a){var b,c=Xa.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Xa.propHooks._default.set(this),this}},Xa.prototype.init.prototype=Xa.prototype,Xa.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Xa.propHooks.scrollTop=Xa.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Xa.prototype.init,r.fx.step={};var Ya,Za,$a=/^(?:toggle|show|hide)$/,_a=/queueHooks$/;function ab(){Za&&(a.requestAnimationFrame(ab),r.fx.tick())}function bb(){return a.setTimeout(function(){Ya=void 0}),Ya=r.now()}function cb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=aa[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function db(a,b,c){for(var d,e=(gb.tweeners[b]||[]).concat(gb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?hb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K); +if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),hb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ib[b]||r.find.attr;ib[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ib[g],ib[g]=e,e=null!=c(a,b,d)?g:null,ib[g]=f),e}});var jb=/^(?:input|select|textarea|button)$/i,kb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):jb.test(a.nodeName)||kb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});var lb=/[\t\r\n\f]/g;function mb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,mb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,mb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,mb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=mb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(c)+" ").replace(lb," ").indexOf(b)>-1)return!0;return!1}});var nb=/\r/g,ob=/[\x20\t\r\n\f]+/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(nb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:r.trim(r.text(a)).replace(ob," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type,g=f?null:[],h=f?e+1:d.length,i=e<0?h:f?e:0;i-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ha.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,""),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    \ No newline at end of file diff --git a/src/main/webapp/plugins/kanbanniang/plugin.properties b/src/main/webapp/plugins/kanbanniang/plugin.properties new file mode 100644 index 00000000..3fbb00af --- /dev/null +++ b/src/main/webapp/plugins/kanbanniang/plugin.properties @@ -0,0 +1,29 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Description of plugin kanbanniang. +# Version: 1.0.0.1, Jul 29, 2019 +# Author: Liyuan Li +# +rendererId=footer.ftl +author=Vanessa +name=\u770B\u677F\u5A18 +version=0.0.1 +types=PUBLIC +pluginClass=org.b3log.latke.plugin.NotInteractivePlugin diff --git a/src/main/webapp/plugins/list/plugin.properties b/src/main/webapp/plugins/list/plugin.properties new file mode 100644 index 00000000..d49169d9 --- /dev/null +++ b/src/main/webapp/plugins/list/plugin.properties @@ -0,0 +1,29 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Table of contents generator. +# Version: 1.0.0.2, Jul 29, 2019 +# Author: Liang Ding +# +rendererId=footer.ftl +author=88250 +name=Table of Contents Generator +version=1.0.1 +types=PUBLIC +pluginClass=org.b3log.solo.plugin.ToCPlugin diff --git a/src/main/webapp/plugins/symphony-interest/lang_en_US.properties b/src/main/webapp/plugins/symphony-interest/lang_en_US.properties new file mode 100644 index 00000000..7e99c7a1 --- /dev/null +++ b/src/main/webapp/plugins/symphony-interest/lang_en_US.properties @@ -0,0 +1,25 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Language configurations(en_US) of plugin symphony-interest. +# Version: 1.0.0.1, Feb 20, 2016 +# Author: Liang Ding +# + +interestLabel=Interest \ No newline at end of file diff --git a/src/main/webapp/plugins/symphony-interest/lang_zh_CN.properties b/src/main/webapp/plugins/symphony-interest/lang_zh_CN.properties new file mode 100644 index 00000000..f2e80a9c --- /dev/null +++ b/src/main/webapp/plugins/symphony-interest/lang_zh_CN.properties @@ -0,0 +1,25 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Language configurations(zh_CN) of plugin symphony-interest. +# Version: 1.0.0.1, Feb 20, 2016 +# Author: Liang Ding +# + +interestLabel=\u793e\u533a\u63a8\u8350 diff --git a/src/main/webapp/plugins/symphony-interest/plugin.ftl b/src/main/webapp/plugins/symphony-interest/plugin.ftl new file mode 100644 index 00000000..ffddf482 --- /dev/null +++ b/src/main/webapp/plugins/symphony-interest/plugin.ftl @@ -0,0 +1,85 @@ +<#-- + + Solo - A small and beautiful blogging system written in Java. + Copyright (c) 2010-present, b3log.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +--> + +
    +
    +
    +

    ${interestLabel}

    +
    +
    +
    +
    +
    +
    +
    + diff --git a/src/main/webapp/plugins/symphony-interest/plugin.properties b/src/main/webapp/plugins/symphony-interest/plugin.properties new file mode 100644 index 00000000..674e90b1 --- /dev/null +++ b/src/main/webapp/plugins/symphony-interest/plugin.properties @@ -0,0 +1,29 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Description of plugin symphony-interest. +# Version: 1.0.0.1, Jul 29, 2019 +# Author: Liang Ding +# +rendererId=admin-main.ftl +author=88250 +name=Symphony Interest +version=0.0.1 +types=ADMIN +pluginClass=org.b3log.latke.plugin.NotInteractivePlugin diff --git a/src/main/webapp/plugins/symphony-interest/style.css b/src/main/webapp/plugins/symphony-interest/style.css new file mode 100644 index 00000000..58dc4176 --- /dev/null +++ b/src/main/webapp/plugins/symphony-interest/style.css @@ -0,0 +1,16 @@ +/* + * plugin style for symphony-interest + * + * @author Liyuan Li + * @version 1.0.0.1, Aug 9, 2011 +*/ +#symphonyInterest ul { + list-style: none; +} +#symphonyInterest a { + text-decoration: none; +} +#symphonyInterest .date { + color: #686868; + font-size: 12px; +} \ No newline at end of file diff --git a/src/main/webapp/robots.txt b/src/main/webapp/robots.txt new file mode 100644 index 00000000..e49b959b --- /dev/null +++ b/src/main/webapp/robots.txt @@ -0,0 +1,7 @@ +User-agent: * +Disallow: /*.do +Disallow: /start +Disallow: /logout +Disallow: /authors/* +Disallow: /console/* +Disallow: /error/* diff --git a/src/main/webapp/scss/_function.scss b/src/main/webapp/scss/_function.scss new file mode 100644 index 00000000..e77fe969 --- /dev/null +++ b/src/main/webapp/scss/_function.scss @@ -0,0 +1,105 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Function and font text style + * + * @author Liyuan Li + * @version 0.3.0.0, Jul 17, 2019 + */ + +.fn { + &__flex { + display: flex; + + &-center { + align-self: center; + } + + &-inline { + display: inline-flex; + align-items: center; + } + + &-1 { + flex: 1; + min-width: 1px; + } + + &-column { + min-height: 100%; + display: flex; + flex-direction: column; + } + } + + &__pointer { + cursor: pointer; + } + + &__clear:before, + &__clear:after { + display: table; + content: ""; + } + + &__clear:after { + clear: both; + } + + &__left { + float: left; + } + + &__right { + float: right; + } + + &__none { + display: none; + } + + &__hidden { + visibility: hidden; + } + + &__ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: normal; + } +} + +.ft { + + &__13 { + font-size: 13px; + } + + &__smaller { + font-size: 12px; + } + + &__center { + text-align: center; + } + + &__nowrap { + white-space: nowrap; + } +} \ No newline at end of file diff --git a/src/main/webapp/scss/_nprogress.scss b/src/main/webapp/scss/_nprogress.scss new file mode 100644 index 00000000..7e4e1d2e --- /dev/null +++ b/src/main/webapp/scss/_nprogress.scss @@ -0,0 +1,96 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Pjax progress + * + * @author Liyuan Li + * @version 0.1.0.0, Mar 20, 2019 + */ + +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #d23f31; + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 2px; +} + +/* Fancy blur effect */ +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #d23f31, 0 0 5px #d23f31; + opacity: 1.0; + + -webkit-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +/* Remove these to get rid of the spinner */ +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 15px; + right: 15px; +} + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: #d23f31; + border-left-color: #d23f31; + border-radius: 50%; + + -webkit-animation: nprogress-spinner 400ms linear infinite; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@-webkit-keyframes nprogress-spinner { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} +@keyframes nprogress-spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/src/main/webapp/scss/_reset.scss b/src/main/webapp/scss/_reset.scss new file mode 100644 index 00000000..1ec2a94d --- /dev/null +++ b/src/main/webapp/scss/_reset.scss @@ -0,0 +1,181 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Common style for reset + * + * @author Liyuan Li + * @version 0.2.0.2, Mar 20, 2019 + */ +html { + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + height: 100%; +} + +body { + margin: 0; + font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif ; + font-size: 14px; + background-color: #fff; + -webkit-font-smoothing: antialiased; + -webkit-overflow-scrolling: touch; +} + +::-moz-selection { + text-shadow: none; + background: rgba(65, 131, 196, 0.4); +} + +::selection { + text-shadow: none; + background: rgba(66, 133, 244, 0.4) +} + +ul, +ol { + margin: 0; + padding: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6, +dl, +dd, +p { + margin: 0; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; +} + +audio:not([controls]) { + display: none; +} + +a { + outline: 0; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + max-width: 100%; + vertical-align: middle; + border: 0; + height: auto; + -ms-interpolation-mode: bicubic; + overflow: hidden; + font-size: 12px; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; + font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif; + outline: none; +} + +button, +input { + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type="search"] { + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + resize: vertical; +} + +svg { + fill: currentColor; + display: inline-block; + stroke-width: 0; + stroke: currentColor; + width: 14px; + height: 14px; +} + +blockquote { + margin: 0; +} \ No newline at end of file diff --git a/src/main/webapp/scss/_toc.scss b/src/main/webapp/scss/_toc.scss new file mode 100644 index 00000000..bf716e41 --- /dev/null +++ b/src/main/webapp/scss/_toc.scss @@ -0,0 +1,58 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * toc + * + * @author Liyuan Li + * @version 0.1.0.0, Mar 20, 2019 + */ +// toc +.article__toc { + overflow: auto; + + &::-webkit-scrollbar { + display: none; + } + + li { + list-style-type: none; + + a { + padding-left: 10px; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + &:hover { + text-decoration: none; + } + } + + &.toc__h3 a { + padding-left: 20px; + } + + &.toc__h4 a { + padding-left: 30px; + } + + &.toc__h5 a { + padding-left: 40px; + } + } +} \ No newline at end of file diff --git a/src/main/webapp/scss/_usite.scss b/src/main/webapp/scss/_usite.scss new file mode 100644 index 00000000..af918741 --- /dev/null +++ b/src/main/webapp/scss/_usite.scss @@ -0,0 +1,29 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * uesr site + * + * @author Liyuan Li + * @version 0.1.0.0, Mar 20, 2019 + */ + +.user__site { + &:hover { + text-decoration: none; + } +} \ No newline at end of file diff --git a/src/main/webapp/scss/admin.css b/src/main/webapp/scss/admin.css new file mode 100644 index 00000000..e0956a07 --- /dev/null +++ b/src/main/webapp/scss/admin.css @@ -0,0 +1 @@ +html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;height:100%}body{margin:0;font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;font-size:14px;background-color:#fff;-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch}::-moz-selection{text-shadow:none;background:rgba(65,131,196,0.4)}::selection{text-shadow:none;background:rgba(66,133,244,0.4)}ul,ol{margin:0;padding:0}h1,h2,h3,h4,h5,h6,dl,dd,p{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none}a{outline:0;text-decoration:none}a:hover{text-decoration:underline}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{max-width:100%;vertical-align:middle;border:0;height:auto;-ms-interpolation-mode:bicubic;overflow:hidden;font-size:12px}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;outline:none}button,input{line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;resize:vertical}svg{fill:currentColor;display:inline-block;stroke-width:0;stroke:currentColor;width:14px;height:14px}blockquote{margin:0}.fn__flex{display:flex}.fn__flex-center{align-self:center}.fn__flex-inline{display:inline-flex;align-items:center}.fn__flex-1{flex:1;min-width:1px}.fn__flex-column{min-height:100%;display:flex;flex-direction:column}.fn__pointer{cursor:pointer}.fn__clear:before,.fn__clear:after{display:table;content:""}.fn__clear:after{clear:both}.fn__left{float:left}.fn__right{float:right}.fn__none{display:none}.fn__hidden{visibility:hidden}.fn__ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-wrap:normal}.ft__13{font-size:13px}.ft__smaller{font-size:12px}.ft__center{text-align:center}.ft__nowrap{white-space:nowrap}@keyframes tooltip-appear{from{opacity:0}to{opacity:1}}.vditor-tooltipped{position:relative;cursor:pointer}.vditor-tooltipped::after{position:absolute;z-index:1000000;display:none;padding:5px 8px;font-size:11px;font-weight:normal;-webkit-font-smoothing:subpixel-antialiased;color:#fff;text-align:center;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:rgba(0,0,0,0.8);border-radius:3px;line-height:16px;opacity:0}.vditor-tooltipped::before{position:absolute;z-index:1000001;display:none;width:0;height:0;color:rgba(0,0,0,0.8);pointer-events:none;content:"";border:5px solid transparent;opacity:0}.vditor-tooltipped--hover::before,.vditor-tooltipped--hover::after,.vditor-tooltipped:hover::before,.vditor-tooltipped:hover::after,.vditor-tooltipped:active::before,.vditor-tooltipped:active::after,.vditor-tooltipped:focus::before,.vditor-tooltipped:focus::after{display:inline-block;text-decoration:none;animation-name:tooltip-appear;animation-duration:0.15s;animation-fill-mode:forwards;animation-timing-function:ease-in}.vditor-tooltipped__s::after,.vditor-tooltipped__se::after,.vditor-tooltipped__sw::after{top:100%;right:50%;margin-top:5px}.vditor-tooltipped__s::before,.vditor-tooltipped__se::before,.vditor-tooltipped__sw::before{top:auto;right:50%;bottom:-5px;margin-right:-5px;border-bottom-color:rgba(0,0,0,0.8)}.vditor-tooltipped__se::after{right:auto;left:50%;margin-left:-15px}.vditor-tooltipped__sw::after{margin-right:-15px}.vditor-tooltipped__n::after,.vditor-tooltipped__ne::after,.vditor-tooltipped__nw::after{right:50%;bottom:100%;margin-bottom:5px}.vditor-tooltipped__n::before,.vditor-tooltipped__ne::before,.vditor-tooltipped__nw::before{top:-5px;right:50%;bottom:auto;margin-right:-5px;border-top-color:rgba(0,0,0,0.8)}.vditor-tooltipped__ne::after{right:auto;left:50%;margin-left:-15px}.vditor-tooltipped__nw::after{margin-right:-15px}.vditor-tooltipped__s::after,.vditor-tooltipped__n::after{transform:translateX(50%)}.vditor-tooltipped__w::after{right:100%;bottom:50%;margin-right:5px;transform:translateY(50%)}.vditor-tooltipped__w::before{top:50%;bottom:50%;left:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,0.8)}.vditor-tooltipped__e::after{bottom:50%;left:100%;margin-left:5px;transform:translateY(50%)}.vditor-tooltipped__e::before{top:50%;right:-5px;bottom:50%;margin-top:-5px;border-right-color:rgba(0,0,0,0.8)}@keyframes scale-in{0%{opacity:0;transform:scale(0.5)}100%{opacity:1;transform:scale(1)}}.vditor-panel{background-color:#fff;position:absolute;box-shadow:0 1px 2px rgba(0,0,0,0.2);border-radius:3px;padding:5px;z-index:4;font-size:14px;display:none;user-select:none;max-width:320px;min-width:80px;animation-duration:.15s;animation-name:scale-in;animation-timing-function:cubic-bezier(0.2, 0, 0.13, 1.5)}.vditor-panel h1,.vditor-panel h2,.vditor-panel h3,.vditor-panel h4,.vditor-panel h5,.vditor-panel h6{margin:0;cursor:pointer;padding:3px 10px;border-radius:3px;line-height:normal}.vditor-panel h1:hover,.vditor-panel h2:hover,.vditor-panel h3:hover,.vditor-panel h4:hover,.vditor-panel h5:hover,.vditor-panel h6:hover{background-color:#4285f4;color:#fff}.vditor-toolbar{background-color:#f6f8fa;border-bottom:1px solid #d1d5da;padding:0 5px;border-radius:3px 3px 0 0}.vditor-toolbar>div{padding:10px 5px;float:left;line-height:14px;height:36px;box-sizing:border-box}.vditor-toolbar svg{fill:currentColor;display:inline-block;stroke-width:0;stroke:currentColor;width:14px;height:14px}.vditor-toolbar .vditor-tooltipped{color:#586069}.vditor-toolbar .vditor-tooltipped:hover{color:#4285f4}.vditor-toolbar label{overflow:hidden;position:relative;height:14px;width:15px;display:block;cursor:pointer}.vditor-toolbar label svg{position:absolute;top:0;left:0}.vditor-toolbar label input{position:absolute;width:15px;height:15px;top:0;left:0;opacity:.001;overflow:hidden}.vditor-menu--current svg{color:#4285f4}.vditor-menu__divider{width:10px}.vditor-menu__br{width:100%;padding:0 !important;height:0 !important}.vditor-menu--disabled svg{color:rgba(88,96,105,0.6);cursor:not-allowed}.vditor-emojis{display:inline-block;overflow:auto}.vditor-emojis::-webkit-scrollbar{display:none}.vditor-emojis__tip{flex:1;width:200px;margin-right:10px;color:#586069;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.vditor-emojis__tail{margin-top:5px;font-size:12px;color:#586069;display:flex}.vditor-emojis__tail a{text-decoration:none;color:#586069}.vditor-emojis__tail a:hover{color:#4285f4}.vditor-emojis span{cursor:pointer;border-radius:3px;float:left;height:30px;width:30px;text-align:center;line-height:26px;padding:3px;box-sizing:border-box;font-size:16px;transition:all 0.15s ease-in-out}.vditor-emojis span:hover{transform:scale(1.2)}.vditor-emojis img{height:20px;width:20px;float:left;margin:3px 0 0 3px}@keyframes slideInDown{from{transform:translate3d(0, -100%, 0);visibility:visible}to{transform:translate3d(0, 0, 0)}}.vditor{display:flex;flex-direction:column;border:1px solid #d1d5da;border-radius:3px;box-sizing:border-box;font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.vditor--fullscreen{position:fixed;top:0;width:100% !important;left:0;height:100vh !important;z-index:90;background-color:#fff}.vditor-content{display:flex;min-height:60px;flex:1;position:relative}.vditor-textarea{font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;margin:0;height:100%;overflow:auto;width:100%;flex:1;border:0;resize:none;padding:10px;box-sizing:border-box;background-color:#fafbfc;outline:0 none;font-size:16px;line-height:22px;color:#24292e;border-radius:0 0 3px 3px;font-variant-ligatures:no-common-ligatures;white-space:pre-wrap;word-break:break-word;word-wrap:break-word}.vditor-textarea[contenteditable="false"]{opacity:0.3;cursor:not-allowed}.vditor-textarea:focus{background-color:#fff}.vditor-textarea:empty::before{content:attr(placeholder);color:rgba(36,41,46,0.68)}.vditor-preview{flex:1;background-color:#fff;overflow:auto;padding:10px;box-shadow:inset 1px 0 #d1d5da;box-sizing:border-box;border-radius:0 0 3px 0}.vditor-preview>div::-webkit-scrollbar{display:none}.vditor-preview>.vditor-reset{margin:0 auto}.vditor-devtools{display:none;background-color:#fff;overflow:auto;flex:1;box-shadow:inset 1px 0 #d1d5da;box-sizing:border-box;border-radius:0 0 3px 0;padding:10px}.vditor-counter{padding:0 3px;position:absolute;bottom:10px;right:20px;color:#24292e;background-color:rgba(255,255,255,0.6);border-radius:3px;font-size:12px;user-select:none;z-index:2}.vditor-counter--error{color:#d23f31;background-color:rgba(210,63,49,0.1)}.vditor-resize{padding:3px 0;cursor:row-resize;user-select:none;position:absolute;width:100%;z-index:2}.vditor-resize--top{top:-3px}.vditor-resize--bottom{bottom:-3px}.vditor-resize>div{height:3px;background-color:#f6f8fa;transition:all 0.15s ease-in-out}.vditor-resize:hover>div,.vditor-resize--selected>div{background-color:#4285f4}.vditor-resize:hover svg,.vditor-resize--selected svg{color:#fff}.vditor-resize svg{fill:currentColor;stroke-width:0;stroke:currentColor;width:13px;height:3px;display:block;margin:0 auto;color:#586069}.vditor-upload{position:absolute;height:3px;left:0;top:-2px;transition:all 0.15s ease-in-out;background-color:rgba(66,133,244,0.8)}.vditor-tip{position:absolute;font-size:12px;top:10px;color:#fff;animation-duration:.15s;animation-fill-mode:both;width:100%;text-align:center}.vditor-tip--show{display:block;animation-name:slideInDown}.vditor-tip__content{text-align:left;display:inline-block;line-height:16px;padding:3px 10px;border-radius:3px;background:rgba(66,133,244,0.8);position:relative}.vditor-tip__content ul{margin:2px 0;padding:0 0 0 18px}.vditor-tip__close{position:absolute;color:#586069;top:-7px;right:-15px;font-weight:bold;cursor:pointer}.vditor-tip__close:hover{color:#4285f4}.vditor-wysiwyg{background-color:#fafbfc;box-sizing:border-box;flex:1;margin:0}.vditor-wysiwyg[contenteditable="false"]{opacity:0.3;cursor:not-allowed}.vditor-wysiwyg:focus{outline:none;background-color:#fff}.vditor-wysiwyg .marker{height:0;width:0;display:inline-block;overflow:hidden;transition:color .15s ease-in-out}.vditor-wysiwyg .newline{display:none}.vditor-wysiwyg .node--expand .marker{width:auto;height:auto;display:inline;color:#bbb}.vditor-wysiwyg p:empty:after{content:'\200b'}.vditor-wysiwyg li p{display:inline-block}.vditor-hint{background-color:#fff;position:absolute;box-shadow:0 1px 2px rgba(0,0,0,0.2);border-radius:3px;padding:5px 0;z-index:3;line-height:20px;list-style:none;color:#24292e;font-size:12px;margin:0;max-width:250px;min-width:80px;display:none}.vditor-hint li{cursor:pointer;padding:3px 10px;border-bottom:1px solid #d1d5da;line-height:20px}.vditor-hint li:last-child{border-bottom:0}.vditor-hint--current,.vditor-hint li:hover{background-color:#4285f4;color:#fff}.vditor-hint__emoji{font-size:16px;float:left;margin-right:3px}.vditor-hint img{height:20px;width:20px;float:left;margin-right:3px}.vditor-reset{font-variant-ligatures:no-common-ligatures;font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;word-wrap:break-word;overflow:auto;line-height:1.65;font-size:16px;word-break:break-word}.vditor-reset ul ul ul{list-style-type:square}.vditor-reset ul ul{list-style-type:circle}.vditor-reset ul{list-style-type:disc}.vditor-reset ul,.vditor-reset ol{padding-left:2em;margin-top:0;margin-bottom:16px}.vditor-reset li{margin-top:0.25em}.vditor-reset audio{max-width:100%}.vditor-reset audio:focus{outline:none}.vditor-reset video{max-height:90vh;max-width:100%}.vditor-reset img{max-width:100%}.vditor-reset img.emoji{cursor:auto;max-width:20px;vertical-align:sub}.vditor-reset h1,.vditor-reset h2,.vditor-reset h3,.vditor-reset h4,.vditor-reset h5,.vditor-reset h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.vditor-reset h1{padding-bottom:0.3em;font-size:1.7em;border-bottom:1px solid #eee}.vditor-reset h2{padding-bottom:0.3em;font-size:1.5em;border-bottom:1px solid #eee}.vditor-reset h3{font-size:1.25em}.vditor-reset h4{font-size:1em}.vditor-reset h5{font-size:0.875em}.vditor-reset h6{font-size:0.85em}.vditor-reset hr{height:2px;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.vditor-reset p{margin-top:0;margin-bottom:16px}.vditor-reset blockquote{padding:0 1em;color:#777;border-left:0.25em solid #ddd;margin:0 0 16px 0}.vditor-reset blockquote p{margin:0}.vditor-reset ins>iframe{border:0}.vditor-reset iframe{border:1px solid #d1d5da;max-width:100%;box-sizing:border-box}.vditor-reset iframe.iframe__video{min-width:80%;min-height:36vh}.vditor-reset table{width:100%;border:1px solid #dedede;margin:15px auto;border-collapse:collapse;empty-cells:show}.vditor-reset thead{text-align:center}.vditor-reset td,.vditor-reset th{height:35px;border:1px solid #dedede;padding:0 10px}.vditor-reset th{font-weight:bold;text-align:center !important;background:rgba(158,188,226,0.2)}.vditor-reset tbody tr:nth-child(2n){background:rgba(158,188,226,0.12)}.vditor-reset tr:hover{background:#efefef}.vditor-reset code:not(.hljs){padding:0.2em 0.4em;margin:0;font-size:85%;border-radius:3px;font-family:mononoki,Consolas,"Liberation Mono",Menlo,Courier,monospace;word-break:break-word;background-size:20px 20px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==);white-space:pre-wrap;background-color:rgba(27,31,35,0.05)}.vditor-reset pre>code{margin:0;font-size:85%;padding:0.5em;border-radius:5px;display:block;overflow:auto;white-space:pre;font-family:mononoki,Consolas,"Liberation Mono",Menlo,Courier,monospace;background-size:20px 20px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==)}.vditor-reset pre:hover div.vditor-copy{display:block}.vditor-reset kbd{display:inline-block;padding:3px 5px;font:11px Consolas, "Liberation Mono", Menlo, Courier, monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:solid 1px #d1d5da;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.vditor-reset summary{cursor:pointer}.vditor-reset summary:focus{outline:none}.vditor-reset svg{height:auto;width:auto}.vditor-reset p:last-child,.vditor-reset blockquote:last-child,.vditor-reset pre:last-child,.vditor-reset ul:last-child,.vditor-reset ol:last-child,.vditor-reset hr:last-child{margin-bottom:0}.vditor-reset .language-echarts{overflow:hidden;height:420px}.vditor-task{list-style:none;margin-left:-1.4em;display:flex;align-items:center}.vditor-task input{margin-right:5px}.vditor-copy{position:relative;display:none}.vditor-copy textarea{position:absolute;left:-100000px}.vditor-copy span{cursor:pointer;position:absolute;right:0.5em;top:0.5em;height:14px;width:14px;display:block;background-color:#f6f8fa;border-radius:3px;padding:3px}.vditor-copy svg{color:#586069;height:14px;width:14px !important;display:block;fill:currentColor}.vditor--error{color:#d23f31;font-size:12px;display:block;line-height:16px}body{color:#767676;background:#f1f2f7;font-size:13px}a{color:#4183c4;text-decoration:none}a:visited{color:#7ba9d6}a:active{color:#2c5d8d}a:hover{text-decoration:underline}button,.completed-ck span{padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;text-align:center;white-space:nowrap;vertical-align:baseline;cursor:pointer;user-select:none;background-image:none;border:1px solid #1fb5ad;border-radius:4px;background-color:#1fb5ad;color:#fff;height:auto;outline:none;line-height:18px}button.small,.completed-ck span.small{padding:3px 6px;font-size:12px}button:hover{background-color:#1ca59e;border-color:#1ca59e}.fn__margin12{margin:12px}@font-face{font-family:'icomoon';src:url("fonts/icomoon.eot?pmeuih");src:url("fonts/icomoon.eot?pmeuih#iefix") format("embedded-opentype"),url("fonts/icomoon.ttf?pmeuih") format("truetype"),url("fonts/icomoon.woff?pmeuih") format("woff"),url("fonts/icomoon.svg?pmeuih#icomoon") format("svg");font-weight:normal;font-style:normal}[class^="icon-"],[class*=" icon-"]{font-family:'icomoon' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-close:before{content:"\e909"}.icon-move-down:before{content:"\e90a"}.icon-move-up:before{content:"\e90b"}.icon-unordered-list:before{content:"\e004"}.icon-info:before{content:"\e00d"}.icon-article:before{content:"\e61d"}.icon-setting:before{content:"\e604"}.icon-cmts:before{content:"\e60a"}.icon-refresh:before{content:"\e60b"}.icon-chevron-down:before{content:"\e618"}.icon-chevron-up:before{content:"\e619"}a[class^="icon-"],a[class*=" icon-"]{color:#333}a[class^="icon-"]:hover,a[class*=" icon-"]:hover{text-decoration:none;color:#4183c4}.aboutIcon{background-position:-16px -48px}.icon-move-down,.icon-move-up{cursor:pointer}#allPanel{height:auto;min-height:100%;position:relative}#tabsPanel{margin:50px 0 0 240px;padding:15px;min-height:100px}#tabsPanel>div{padding-bottom:36px}#tabsPanel_article-list,#tabsPanel_draft-list,#tabsPanel_comment-list,#tabsPanel_plugin-list,#tabsPanel_page-list>div:first-child,#tabsPanel_user-list>div:first-child,#tabsPanel_category-list>div:first-child,#tabsPanel_link-list>div:first-child{background-color:#FFF;border-radius:4px;margin-bottom:40px;overflow:hidden}.tip{font-weight:bold;margin:0 auto;overflow:hidden;padding:2px 0;position:fixed;text-align:center;top:3px;left:255px;z-index:1001}#tipMsg,#loadMsg{color:#31708f;background-color:#d9edf7;border-radius:4px;line-height:40px;display:inline-block}.footer{bottom:12px;position:absolute;text-align:center;width:100%;padding-left:240px;box-sizing:border-box}#top{height:50px;position:fixed;box-shadow:1px 0 3px rgba(0,0,0,0.15);width:100%;top:0;background-color:#fff;z-index:80}#top>a{background:#1fb5ad;float:left;width:240px;height:50px;position:relative;color:#F7F7F7;text-align:center;text-decoration:none;font-size:20px;line-height:50px}#top>.fn__right a{color:#555;line-height:32px;margin:8px 10px;border:1px solid #f6f6f6;background-color:#f6f6f6;border-radius:100px;padding:0 10px;text-decoration:none;display:inline-block}#top>.fn__right a:hover{color:#333}#top .avatar{height:25px;width:25px;border-radius:20px;background-size:cover;background-repeat:no-repeat;background-position:center center;float:left;margin:3px 6px 0 0}#tabs{height:100%;min-height:468px;width:240px;margin-top:50px;position:fixed;top:0;background-color:#32323a;overflow:auto}#tabs ul{list-style:none}#tabs li{margin-left:0px;border-bottom:1px solid rgba(255,255,255,0.05)}#tabs li>div>a,#tabs #tabToolsTitle,#tabs #tabArticleTitle{color:#aeb2b7;display:block;text-decoration:none;letter-spacing:1px;padding:18px 0 18px 25px;line-height:18px;height:18px;transition:all 0.3s ease;cursor:pointer}#tabs a:hover,#tabs a.tab-current,#tabs #tabToolsTitle.tab-current,#tabs #tabArticleTitle.tab-current,#tabs #tabToolsTitle:hover,#tabs #tabArticleTitle:hover{background-color:#28282e;color:#1fb5ad}#tabs li li{border-bottom:0}#tabs li li>div>a{padding:13px 0 13px 46px;line-height:15px;height:15px;background-color:#28282e}#tabs li li>div>a:hover,#tabs li li>div>a.tab-current{background-color:#202025}#tabs .commentIcon,#tabs .postIcon,#tabs .preferenceIcon,#tabs .usersIcon,#tabs .aboutIcon{display:none}#tabs .icon-chevron-up,#tabs .icon-chevron-down{margin-right:10px}.sub-tabs{background-color:#e0e1e7;border-radius:4px 4px 0 0;overflow:hidden}.sub-tabs ul{float:left;list-style:none outside none;margin-right:24px}.sub-tabs li{float:left;margin:0}.sub-tabs li a{border-right:1px solid #e0e1e7;color:#898989;display:block;line-height:18px;padding:20px 15px;text-decoration:none}.sub-tabs .tab-current,.sub-tabs .tab-current:hover,.sub-tabs a:hover{background-color:#FFFFFF;color:#1fb5ad}.sub-tabs-main{background-color:#FFFFFF;padding:15px;border-radius:0 0 4px 4px}.form>div{margin:15px 0}.form label{margin-bottom:10px;display:inline-block}.form label.checkbox{display:inline-flex;align-items:center;margin:11px 0}.form label.checkbox input{height:auto}.form input[type='text'],.form input[type='password'],.form textarea,.form select{font-size:14px;outline:medium none;width:100%;padding:6px 12px;height:34px;box-sizing:border-box;border:1px solid #e2e2e4;color:#333;border-radius:4px;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;background-color:#fff}.form textarea{height:auto}.form input[type='text']:focus,.form input[type='radio']:focus,.form input[type='password']:focus,.form textarea:focus{box-shadow:none;border-color:#1fb5ad}table.form label{margin:10px 0 0 0}table.form th{vertical-align:initial}.module-panel{border-radius:4px;background-color:#fff;margin-bottom:20px;min-height:160px}.module-header{background:#fafafa;padding:15px;border-bottom:1px solid #eff2f7;border-top-left-radius:3px;border-top-right-radius:3px}.module-header h2{font-size:13px;font-weight:400}.module-header a{color:#767676;text-decoration:none}.module-header a:hover{color:#1fb5ad}.module-panel .module-body{padding:15px}.module-panel .module-body li{padding:10px;line-height:18px;border-bottom:1px solid #ddd}.module-panel .module-body li:hover{background-color:#fafafa}.module-panel .module-body li a:hover{text-decoration:none}.comment-title{background-color:#ECECEC;border-bottom:1px solid #DFDFDF;border-top:1px solid #F9F9F9;padding:3px 6px 3px 12px;line-height:24px}#comments{height:462px;overflow:auto}.article__thumbnail{margin:10px auto 20px;display:none;text-align:center}.article__thumbnail .thumbnail__img{background-color:rgba(0,0,0,0.02);background-size:cover;background-repeat:no-repeat;background-position:50%;height:432px;width:768px;margin:0 auto}.article__thumbnail button{margin-top:10px}button#submitArticle{background-color:#fa8564;border-color:#fa8564}button#submitArticle:hover{background-color:#ec6459;border-color:#ec6459}#skinsPanel{min-width:328px;width:99%}.skinPreview{height:160px;width:280px}.skinItem{background-color:#FAFAFA;border-radius:3px;box-shadow:0 0 1px rgba(0,0,0,0.3);font-size:16px;line-height:24px;margin:10px;text-align:center}.skinItem button:first-child{margin-left:10px}#skinMain .skinItem.selected,.skinItem:hover{background-color:#1fb5ad;box-shadow:0px 0px 6px rgba(0,0,0,0.3)}.f-blue,.error-msg{color:#3366CC}.signs button{margin:0 12px}.small-head{float:left;height:35px;margin:3px 9px 0 3px;width:35px}#commentTable .table-main td{vertical-align:top}.about-logo{margin:0 40px 0 20px;align-self:center}.about-margin{font-size:18px;margin:20px 0}.about__panel .about-list li{border:0;padding:0;margin-left:24px}.about__panel .about-list li:hover{background-color:#fff;text-decoration:underline}.ft__fade{color:rgba(0,0,0,0.38)}.ft__green{color:#569e3d}.tag__select{position:relative;display:block}.tag__select button{position:absolute;right:0;top:1px;border-radius:0 4px 4px 0}.completed-panel{background-color:#fff;border:1px solid #ddd;color:#0000CC;display:none;overflow:auto;padding:2px;position:absolute;z-index:10;width:50%;border-top:0}.completed-panel a{display:block;text-decoration:none;line-height:26px;padding:0 5px}.completed-panel a.selected{background-color:#fafafa;color:#fa8564}.completed-ck{margin-top:9px}.completed-ck span,.signs button,.skinItem button{margin:5px 10px 5px 0;display:inline-block;background-color:#c7cbd6;border-color:#c7cbd6}.completed-ck span:hover,.signs button:hover,.skinItem button:hover{background-color:#b0b5b9;border-color:#b0b5b9}.completed-ck span.selected,.signs button.selected{background-color:#1ca59e;border-color:#1ca59e;color:#fff}.dialog-background{background-color:#000000;display:none;filter:alpha(opacity=30);height:100%;left:0;opacity:.3;position:fixed;top:0;width:100%;z-index:90}.dialog-panel{display:none;position:absolute;z-index:100}.dialog-title{color:#FFFFFF;float:left;font-size:12px;margin-left:12px}.dialog-header-bg{background:#32323a;border-radius:6px 6px 0 0;cursor:move;height:34px;line-height:34px}.dialog-header-bg .icon-close{cursor:pointer;float:right;height:18px;margin-top:6px;padding:3px;width:22px;color:#1fb5ad}.dialog-header-bg .icon-close:hover{color:#1ca59e}.dialog-main{background-color:#FFFFFF;border:1px solid #666666;border-color:#C0C0C0 #D9D9D9 #D9D9D9;border-top-width:0;padding:12px}.paginate-paginate div.button.paginate-page,.paginate-paginate a,.paginate-pageCount,.pagination-pages{border:1px solid #EFF2F7;padding:6px 12px;cursor:pointer;float:left;line-height:18px;background:#eee;height:18px;margin-left:1px;border-radius:0;text-decoration:none;color:#333}.pagination-current-page,.paginate-pageCount{cursor:auto}.paginate-pageCount{margin-left:10px}.pagination-current-page,.paginate-paginate a:hover,.paginate-paginate div.button.paginate-page:hover,.pagination-pages:hover{border-color:#2eb4ad;background-color:#2eb4ad;color:#FFF}.paginate-paginate div.button{padding:0;float:left;border:0;background:none}.paginate-inputPage{height:26px;width:24px;margin:0 5px}.table-main{margin:0;padding:0;position:relative}.table-main table{word-break:break-all;word-wrap:break-word}.table-header{border-bottom:1px solid #ddd;background-color:#fafafa;padding:15px 0}.table-header th{line-height:18px;text-align:left;text-indent:6px;font-weight:normal}.table-main td{border-bottom:1px solid #ddd;color:#767676;padding:10px 0}.table-lineHover .table-hasExpend td{border-bottom:0px}.table-heiglight td,.table-lineHover td{background-color:#fafafa;border-right-color:#fafafa}.table-expendRow td{padding:0 0 5px 36px}.table-expendRow td span{margin-left:60px}.table-expendRow td a{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px;text-decoration:none;color:#FFF;background-color:#c7cbd6;border-color:#c7cbd6}.table-expendRow td a:hover{background-color:#b0b5b9;border-color:#b0b5b9}.table-center{margin:0 auto}.table-tag{color:#D54121;font-size:12px;font-style:italic;margin-left:10px}.navigation-icon{height:18px;width:18px;float:left;margin:-1px 10px 0 0}#preferenceSignButton1,#preferenceSignButton2,#preferenceSignButton3{margin:10px 0}#tabPreferencePanel label,#tabPreferencePanel_setting label,.form__no-table label{width:100%;margin-top:10px;font-weight:bold}.page-list__label{width:100%}.search-btn{position:absolute;top:28px;right:27px;border-radius:0 4px 4px 0}.vditor-toolbar label{margin-bottom:0}.vditor .vditor-textarea{border:0;resize:none;padding:10px;box-sizing:border-box;background-color:#fafbfc;outline:0 none;font-size:16px;line-height:22px;color:#24292e;border-radius:0 0 3px 3px}@media (max-width: 768px){#top>a{display:none}#tabsPanel{margin-left:0}.footer{padding-left:0}.skinPreview{width:100%;height:auto;margin:0}.tip{left:12px}#tabs{transition:all 0.15s ease-in-out;left:-240px;z-index:100}.top__menu{font-size:22px;margin:12px;float:left;display:block}.fn__flex{display:block}#permalink{margin:0 !important}.permalink__label{margin-bottom:10px !important}.viewpwd__panel,.article-commentable__panel{float:none}#viewPwd{width:100% !important}.viewpwd__panel label{margin-top:20px}.signs label{width:100%}.table-expendRow td{padding-left:10px}.about-margin{margin-bottom:10px}.about__iframe{margin:0 0 20px 0 !important}.about-logo{margin:0 auto;display:block;float:none;width:128px}.about__panel{margin-left:0 !important}.dialog-panel{left:0 !important;top:0 !important;padding:10px !important;box-sizing:border-box !important;width:100% !important}.tabs__bg{display:none;width:100vw;height:100vh;background-color:rgba(0,0,0,0.4);position:fixed;top:0;z-index:78}} diff --git a/src/main/webapp/scss/admin.scss b/src/main/webapp/scss/admin.scss new file mode 100644 index 00000000..9499a61a --- /dev/null +++ b/src/main/webapp/scss/admin.scss @@ -0,0 +1,1053 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** +* admin style +* +* @author Liyuan Li +* @author Liang Ding +* @version 3.2.0.0, Jul 17, 2019 +*/ +@import "reset"; +@import "function"; +@import "vditor/src/assets/scss/classic"; + +body { + color: #767676; + background: #f1f2f7; + font-size: 13px; +} + +a { + color: #4183c4; + text-decoration: none +} + +a:visited { + color: #7ba9d6 +} + +a:active { + color: #2c5d8d +} + +a:hover { + text-decoration: underline +} + +button, +.completed-ck span { + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + cursor: pointer; + user-select: none; + background-image: none; + border: 1px solid #1fb5ad; + border-radius: 4px; + background-color: #1fb5ad; + color: #fff; + height: auto; + outline: none; + line-height: 18px; + + &.small { + padding: 3px 6px; + font-size: 12px; + } +} + +button:hover { + background-color: #1ca59e; + border-color: #1ca59e; +} + +.fn__margin12 { + margin: 12px; +} + +/* start ico */ +@font-face { + font-family: 'icomoon'; + src: url('fonts/icomoon.eot?pmeuih'); + src: url('fonts/icomoon.eot?pmeuih#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?pmeuih') format('truetype'), + url('fonts/icomoon.woff?pmeuih') format('woff'), + url('fonts/icomoon.svg?pmeuih#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-close:before { + content: "\e909"; +} + +.icon-move-down:before { + content: "\e90a"; +} + +.icon-move-up:before { + content: "\e90b"; +} + +.icon-unordered-list:before { + content: "\e004"; +} + +.icon-info:before { + content: "\e00d"; +} + +.icon-article:before { + content: "\e61d"; +} + +.icon-setting:before { + content: "\e604"; +} + +.icon-cmts:before { + content: "\e60a"; +} + +.icon-refresh:before { + content: "\e60b"; +} + +.icon-chevron-down:before { + content: "\e618"; +} + +.icon-chevron-up:before { + content: "\e619"; +} + +a[class^="icon-"], +a[class*=" icon-"] { + color: #333; +} + +a[class^="icon-"]:hover, +a[class*=" icon-"]:hover { + text-decoration: none; + color: #4183c4; +} + +.aboutIcon { + background-position: -16px -48px; +} + +.icon-move-down, +.icon-move-up { + cursor: pointer; +} + +/* end ico */ + +/* start frame */ +#allPanel { + height: auto; + min-height: 100%; + position: relative; +} + +#tabsPanel { + margin: 50px 0 0 240px; + padding: 15px; + min-height: 100px; +} + +#tabsPanel > div { + padding-bottom: 36px; +} + +#tabsPanel_article-list, +#tabsPanel_draft-list, +#tabsPanel_comment-list, +#tabsPanel_plugin-list, +#tabsPanel_page-list > div:first-child, +#tabsPanel_user-list > div:first-child, +#tabsPanel_category-list > div:first-child, +#tabsPanel_link-list > div:first-child { + background-color: #FFF; + border-radius: 4px; + margin-bottom: 40px; + overflow: hidden; +} + +.tip { + font-weight: bold; + margin: 0 auto; + overflow: hidden; + padding: 2px 0; + position: fixed; + text-align: center; + top: 3px; + left: 255px; + z-index: 1001; +} + +#tipMsg, #loadMsg { + color: #31708f; + background-color: #d9edf7; + border-radius: 4px; + line-height: 40px; + display: inline-block; +} + +.footer { + bottom: 12px; + position: absolute; + text-align: center; + width: 100%; + padding-left: 240px; + box-sizing: border-box; +} + +/* end frame */ + +/* start top-nav */ +#top { + height: 50px; + position: fixed; + box-shadow: 1px 0 3px rgba(0, 0, 0, .15); + width: 100%; + top: 0; + background-color: #fff; + z-index: 80; +} + +#top > a { + background: #1fb5ad; + float: left; + width: 240px; + height: 50px; + position: relative; + color: #F7F7F7; + text-align: center; + text-decoration: none; + font-size: 20px; + line-height: 50px; +} + +#top > .fn__right a { + color: #555; + line-height: 32px; + margin: 8px 10px; + border: 1px solid #f6f6f6; + background-color: #f6f6f6; + border-radius: 100px; + padding: 0 10px; + text-decoration: none; + display: inline-block; +} + +#top > .fn__right a:hover { + color: #333; +} + +#top .avatar { + height: 25px; + width: 25px; + border-radius: 20px; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + float: left; + margin: 3px 6px 0 0; +} + +/* end top-nav */ + +/* start first tab */ +#tabs { + height: 100%; + min-height: 468px; + width: 240px; + margin-top: 50px; + position: fixed; + top: 0; + background-color: #32323a; + overflow: auto; +} + +#tabs ul { + list-style: none; +} + +#tabs li { + margin-left: 0px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +#tabs li > div > a, +#tabs #tabToolsTitle, +#tabs #tabArticleTitle { + color: #aeb2b7; + display: block; + text-decoration: none; + letter-spacing: 1px; + padding: 18px 0 18px 25px; + line-height: 18px; + height: 18px; + transition: all 0.3s ease; + cursor: pointer; +} + +#tabs a:hover, +#tabs a.tab-current, +#tabs #tabToolsTitle.tab-current, +#tabs #tabArticleTitle.tab-current, +#tabs #tabToolsTitle:hover, +#tabs #tabArticleTitle:hover { + background-color: #28282e; + color: #1fb5ad; +} + +#tabs li li { + border-bottom: 0; +} + +#tabs li li > div > a { + padding: 13px 0 13px 46px; + line-height: 15px; + height: 15px; + background-color: #28282e; +} + +#tabs li li > div > a:hover, +#tabs li li > div > a.tab-current { + background-color: #202025; +} + +#tabs .commentIcon, #tabs .postIcon, #tabs .preferenceIcon, #tabs .usersIcon, +#tabs .aboutIcon { + display: none; +} + +#tabs .icon-chevron-up, +#tabs .icon-chevron-down { + margin-right: 10px; +} + +/* end first tabs */ + +/* start sub tabs */ +.sub-tabs { + background-color: #e0e1e7; + border-radius: 4px 4px 0 0; + overflow: hidden; +} + +.sub-tabs ul { + float: left; + list-style: none outside none; + margin-right: 24px; +} + +.sub-tabs li { + float: left; + margin: 0; +} + +.sub-tabs li a { + border-right: 1px solid #e0e1e7; + color: #898989; + display: block; + line-height: 18px; + padding: 20px 15px; + text-decoration: none; +} + +.sub-tabs .tab-current, .sub-tabs .tab-current:hover, +.sub-tabs a:hover { + background-color: #FFFFFF; + color: #1fb5ad; +} + +.sub-tabs-main { + background-color: #FFFFFF; + padding: 15px; + border-radius: 0 0 4px 4px; +} + +/* end preference tabs*/ + +/* start form */ +.form > div { + margin: 15px 0; +} + +.form label { + margin-bottom: 10px; + display: inline-block; +} + +.form label.checkbox { + display: inline-flex; + align-items: center; + margin: 11px 0; +} + +.form label.checkbox input { + height: auto; +} + +.form input[type='text'], .form input[type='password'], .form textarea, +.form select { + font-size: 14px; + outline: medium none; + width: 100%; + padding: 6px 12px; + height: 34px; + box-sizing: border-box; + border: 1px solid #e2e2e4; + color: #333; + border-radius: 4px; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + background-color: #fff; +} + +.form textarea { + height: auto; +} + +.form input[type='text']:focus, +.form input[type='radio']:focus, +.form input[type='password']:focus, .form textarea:focus { + box-shadow: none; + border-color: #1fb5ad; +} + +table.form label { + margin: 10px 0 0 0; +} + +table.form th { + vertical-align: initial; +} + +/* end form */ + +/* start module */ +.module-panel { + border-radius: 4px; + background-color: #fff; + margin-bottom: 20px; + min-height: 160px; +} + +.module-header { + background: #fafafa; + padding: 15px; + border-bottom: 1px solid #eff2f7; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.module-header h2 { + font-size: 13px; + font-weight: 400; +} + +.module-header a { + color: #767676; + text-decoration: none; +} + +.module-header a:hover { + color: #1fb5ad; +} + +.module-panel .module-body { + padding: 15px; +} + +.module-panel .module-body li { + padding: 10px; + line-height: 18px; + border-bottom: 1px solid #ddd; +} + +.module-panel .module-body li:hover { + background-color: #fafafa; +} + +.module-panel .module-body li a:hover { + text-decoration: none; +} + +/* end module */ + +/* start comments */ +.comment-title { + background-color: #ECECEC; + border-bottom: 1px solid #DFDFDF; + border-top: 1px solid #F9F9F9; + padding: 3px 6px 3px 12px; + line-height: 24px; +} + +#comments { + height: 462px; + overflow: auto; +} + +/* end comments */ + +/* start article */ +.article__thumbnail { + margin: 10px auto 20px; + display: none; + text-align: center; +} + +.article__thumbnail .thumbnail__img { + background-color: rgba(0, 0, 0, .02); + background-size: cover; + background-repeat: no-repeat; + background-position: 50%; + height: 432px; + width: 768px; + margin: 0 auto; +} + +.article__thumbnail button { + margin-top: 10px +} + +button#submitArticle { + background-color: #fa8564; + border-color: #fa8564; +} + +button#submitArticle:hover { + background-color: #ec6459; + border-color: #ec6459; +} + +/* end article */ + +/* start preference */ +#skinsPanel { + min-width: 328px; + width: 99%; +} + +.skinPreview { + height: 160px; + width: 280px; +} + +.skinItem { + background-color: #FAFAFA; + border-radius: 3px; + box-shadow: 0 0 1px rgba(0, 0, 0, 0.3); + font-size: 16px; + line-height: 24px; + margin: 10px; + text-align: center; + + button:first-child { + margin-left: 10px; + } +} + +#skinMain .skinItem.selected, +.skinItem:hover { + background-color: #1fb5ad; + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); +} + +.f-blue, .error-msg { + color: #3366CC; +} + +.signs button { + margin: 0 12px; +} + +/* end preference */ + +/* start common list */ +.small-head { + float: left; + height: 35px; + margin: 3px 9px 0 3px; + width: 35px; +} + +#commentTable .table-main td { + vertical-align: top; +} + +/* end common list */ + +/* start about */ +.about-logo { + margin: 0 40px 0 20px; + align-self: center; +} + +.about-margin { + font-size: 18px; + margin: 20px 0; +} + +.about__panel .about-list li { + border: 0; + padding: 0; + margin-left: 24px; +} + +.about__panel .about-list li:hover { + background-color: #fff; + text-decoration: underline; +} + +.ft { + &__fade { + color: rgba(0, 0, 0, .38) + } + + &__green { + color: #569e3d; + } +} + +/* end about */ + +/* + * jQuly plugin: bowknot style + * + * @author Liyuan Li + * @version 1.0.0.7, Oct 29, 2011 +*/ +.tag__select { + position: relative; + display: block; +} + +.tag__select button { + position: absolute; + right: 0; + top: 1px; + border-radius: 0 4px 4px 0; +} + +.completed-panel { + background-color: #fff; + border: 1px solid #ddd; + color: #0000CC; + display: none; + overflow: auto; + padding: 2px; + position: absolute; + z-index: 10; + width: 50%; + border-top: 0; +} + +.completed-panel a { + display: block; + text-decoration: none; + line-height: 26px; + padding: 0 5px; +} + +.completed-panel a.selected { + background-color: #fafafa; + color: #fa8564; +} + +.completed-ck { + margin-top: 9px; +} + +.completed-ck span, +.signs button, +.skinItem button { + margin: 5px 10px 5px 0; + display: inline-block; + background-color: #c7cbd6; + border-color: #c7cbd6; +} + +.completed-ck span:hover, +.signs button:hover, +.skinItem button:hover { + background-color: #b0b5b9; + border-color: #b0b5b9; +} + +.completed-ck span.selected, +.signs button.selected { + background-color: #1ca59e; + border-color: #1ca59e; + color: #fff; +} + +.dialog-background { + background-color: #000000; + display: none; + filter: alpha(opacity=30); + height: 100%; + left: 0; + opacity: .3; + position: fixed; + top: 0; + width: 100%; + z-index: 90; +} + +.dialog-panel { + display: none; + position: absolute; + z-index: 100; +} + +.dialog-title { + color: #FFFFFF; + float: left; + font-size: 12px; + margin-left: 12px; +} + +.dialog-header-bg { + background: #32323a; + border-radius: 6px 6px 0 0; + cursor: move; + height: 34px; + line-height: 34px; +} + +.dialog-header-bg .icon-close { + cursor: pointer; + float: right; + height: 18px; + margin-top: 6px; + padding: 3px; + width: 22px; + color: #1fb5ad; +} + +.dialog-header-bg .icon-close:hover { + color: #1ca59e; +} + +.dialog-main { + background-color: #FFFFFF; + border: 1px solid #666666; + border-color: #C0C0C0 #D9D9D9 #D9D9D9; + border-top-width: 0; + padding: 12px; +} + +.paginate-paginate div.button.paginate-page, .paginate-paginate a, .paginate-pageCount, +.pagination-pages { + border: 1px solid #EFF2F7; + padding: 6px 12px; + cursor: pointer; + float: left; + line-height: 18px; + background: #eee; + height: 18px; + margin-left: 1px; + border-radius: 0; + text-decoration: none; + color: #333; +} + +.pagination-current-page, +.paginate-pageCount { + cursor: auto; +} + +.paginate-pageCount { + margin-left: 10px; +} + +.pagination-current-page, +.paginate-paginate a:hover, +.paginate-paginate div.button.paginate-page:hover, +.pagination-pages:hover { + border-color: #2eb4ad; + background-color: #2eb4ad; + color: #FFF; +} + +.paginate-paginate div.button { + padding: 0; + float: left; + border: 0; + background: none; +} + +.paginate-inputPage { + height: 26px; + width: 24px; + margin: 0 5px; +} + +.table-main { + margin: 0; + padding: 0; + position: relative; +} + +.table-main table { + word-break: break-all; + word-wrap: break-word; +} + +.table-header { + border-bottom: 1px solid #ddd; + background-color: #fafafa; + padding: 15px 0; +} + +.table-header th { + line-height: 18px; + text-align: left; + text-indent: 6px; + font-weight: normal; +} + +.table-main td { + border-bottom: 1px solid #ddd; + color: #767676; + padding: 10px 0; +} + +.table-lineHover .table-hasExpend td { + border-bottom: 0px; +} + +.table-heiglight td, .table-lineHover td { + background-color: #fafafa; + border-right-color: #fafafa; +} + +.table-expendRow td { + padding: 0 0 5px 36px; +} + +.table-expendRow td span { + margin-left: 60px; +} + +.table-expendRow td a { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; + text-decoration: none; + color: #FFF; + background-color: #c7cbd6; + border-color: #c7cbd6; +} + +.table-expendRow td a:hover { + background-color: #b0b5b9; + border-color: #b0b5b9; +} + +.table-center { + margin: 0 auto; +} + +.table-tag { + color: #D54121; + font-size: 12px; + font-style: italic; + margin-left: 10px; +} + +.navigation-icon { + height: 18px; + width: 18px; + float: left; + margin: -1px 10px 0 0; +} + +#preferenceSignButton1, +#preferenceSignButton2, +#preferenceSignButton3 { + margin: 10px 0; +} + +#tabPreferencePanel label, +#tabPreferencePanel_setting label, +.form__no-table label { + width: 100%; + margin-top: 10px; + font-weight: bold; +} + +.page-list__label { + width: 100%; +} + +.search-btn { + position: absolute; + top: 28px; + right: 27px; + border-radius: 0 4px 4px 0; +} + +.vditor-toolbar label { + margin-bottom: 0; +} + +.vditor .vditor-textarea { + border: 0; + resize: none; + padding: 10px; + box-sizing: border-box; + background-color: #fafbfc; + outline: 0 none; + font-size: 16px; + line-height: 22px; + color: #24292e; + border-radius: 0 0 3px 3px; +} + +@media (max-width: 768px) { + #top > a { + display: none; + } + + #tabsPanel { + margin-left: 0; + } + + .footer { + padding-left: 0; + } + + .skinPreview { + width: 100%; + height: auto; + margin: 0; + } + + .tip { + left: 12px; + } + + #tabs { + transition: all 0.15s ease-in-out; + left: -240px; + z-index: 100; + } + + .top__menu { + font-size: 22px; + margin: 12px; + float: left; + display: block; + } + + .fn__flex { + display: block; + } + + #permalink { + margin: 0 !important; + } + + .permalink__label { + margin-bottom: 10px !important; + } + + .viewpwd__panel, + .article-commentable__panel { + float: none; + } + + #viewPwd { + width: 100% !important; + } + + .viewpwd__panel label { + margin-top: 20px; + } + + .signs label { + width: 100%; + } + + .table-expendRow td { + padding-left: 10px; + } + + .about-margin { + margin-bottom: 10px; + } + + .about__iframe { + margin: 0 0 20px 0 !important; + } + + .about-logo { + margin: 0 auto; + display: block; + float: none; + width: 128px; + } + + .about__panel { + margin-left: 0 !important; + } + + .dialog-panel { + left: 0 !important; + top: 0 !important; + padding: 10px !important; + box-sizing: border-box !important; + width: 100% !important; + } + + .tabs__bg { + display: none; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.4); + position: fixed; + top: 0; + z-index: 78; + } +} \ No newline at end of file diff --git a/src/main/webapp/scss/base.css b/src/main/webapp/scss/base.css new file mode 100644 index 00000000..9618cbca --- /dev/null +++ b/src/main/webapp/scss/base.css @@ -0,0 +1 @@ +html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;height:100%}body{margin:0;font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;font-size:14px;background-color:#fff;-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch}::-moz-selection{text-shadow:none;background:rgba(65,131,196,0.4)}::selection{text-shadow:none;background:rgba(66,133,244,0.4)}ul,ol{margin:0;padding:0}h1,h2,h3,h4,h5,h6,dl,dd,p{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none}a{outline:0;text-decoration:none}a:hover{text-decoration:underline}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{max-width:100%;vertical-align:middle;border:0;height:auto;-ms-interpolation-mode:bicubic;overflow:hidden;font-size:12px}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;outline:none}button,input{line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;resize:vertical}svg{fill:currentColor;display:inline-block;stroke-width:0;stroke:currentColor;width:14px;height:14px}blockquote{margin:0}@keyframes tooltip-appear{from{opacity:0}to{opacity:1}}.vditor-tooltipped{position:relative;cursor:pointer}.vditor-tooltipped::after{position:absolute;z-index:1000000;display:none;padding:5px 8px;font-size:11px;font-weight:normal;-webkit-font-smoothing:subpixel-antialiased;color:#fff;text-align:center;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:rgba(0,0,0,0.8);border-radius:3px;line-height:16px;opacity:0}.vditor-tooltipped::before{position:absolute;z-index:1000001;display:none;width:0;height:0;color:rgba(0,0,0,0.8);pointer-events:none;content:"";border:5px solid transparent;opacity:0}.vditor-tooltipped--hover::before,.vditor-tooltipped--hover::after,.vditor-tooltipped:hover::before,.vditor-tooltipped:hover::after,.vditor-tooltipped:active::before,.vditor-tooltipped:active::after,.vditor-tooltipped:focus::before,.vditor-tooltipped:focus::after{display:inline-block;text-decoration:none;animation-name:tooltip-appear;animation-duration:0.15s;animation-fill-mode:forwards;animation-timing-function:ease-in}.vditor-tooltipped__s::after,.vditor-tooltipped__se::after,.vditor-tooltipped__sw::after{top:100%;right:50%;margin-top:5px}.vditor-tooltipped__s::before,.vditor-tooltipped__se::before,.vditor-tooltipped__sw::before{top:auto;right:50%;bottom:-5px;margin-right:-5px;border-bottom-color:rgba(0,0,0,0.8)}.vditor-tooltipped__se::after{right:auto;left:50%;margin-left:-15px}.vditor-tooltipped__sw::after{margin-right:-15px}.vditor-tooltipped__n::after,.vditor-tooltipped__ne::after,.vditor-tooltipped__nw::after{right:50%;bottom:100%;margin-bottom:5px}.vditor-tooltipped__n::before,.vditor-tooltipped__ne::before,.vditor-tooltipped__nw::before{top:-5px;right:50%;bottom:auto;margin-right:-5px;border-top-color:rgba(0,0,0,0.8)}.vditor-tooltipped__ne::after{right:auto;left:50%;margin-left:-15px}.vditor-tooltipped__nw::after{margin-right:-15px}.vditor-tooltipped__s::after,.vditor-tooltipped__n::after{transform:translateX(50%)}.vditor-tooltipped__w::after{right:100%;bottom:50%;margin-right:5px;transform:translateY(50%)}.vditor-tooltipped__w::before{top:50%;bottom:50%;left:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,0.8)}.vditor-tooltipped__e::after{bottom:50%;left:100%;margin-left:5px;transform:translateY(50%)}.vditor-tooltipped__e::before{top:50%;right:-5px;bottom:50%;margin-top:-5px;border-right-color:rgba(0,0,0,0.8)}@keyframes scale-in{0%{opacity:0;transform:scale(0.5)}100%{opacity:1;transform:scale(1)}}.vditor-panel{background-color:#fff;position:absolute;box-shadow:0 1px 2px rgba(0,0,0,0.2);border-radius:3px;padding:5px;z-index:4;font-size:14px;display:none;user-select:none;max-width:320px;min-width:80px;animation-duration:.15s;animation-name:scale-in;animation-timing-function:cubic-bezier(0.2, 0, 0.13, 1.5)}.vditor-panel h1,.vditor-panel h2,.vditor-panel h3,.vditor-panel h4,.vditor-panel h5,.vditor-panel h6{margin:0;cursor:pointer;padding:3px 10px;border-radius:3px;line-height:normal}.vditor-panel h1:hover,.vditor-panel h2:hover,.vditor-panel h3:hover,.vditor-panel h4:hover,.vditor-panel h5:hover,.vditor-panel h6:hover{background-color:#4285f4;color:#fff}.vditor-toolbar{background-color:#f6f8fa;border-bottom:1px solid #d1d5da;padding:0 5px;border-radius:3px 3px 0 0}.vditor-toolbar>div{padding:10px 5px;float:left;line-height:14px;height:36px;box-sizing:border-box}.vditor-toolbar svg{fill:currentColor;display:inline-block;stroke-width:0;stroke:currentColor;width:14px;height:14px}.vditor-toolbar .vditor-tooltipped{color:#586069}.vditor-toolbar .vditor-tooltipped:hover{color:#4285f4}.vditor-toolbar label{overflow:hidden;position:relative;height:14px;width:15px;display:block;cursor:pointer}.vditor-toolbar label svg{position:absolute;top:0;left:0}.vditor-toolbar label input{position:absolute;width:15px;height:15px;top:0;left:0;opacity:.001;overflow:hidden}.vditor-menu--current svg{color:#4285f4}.vditor-menu__divider{width:10px}.vditor-menu__br{width:100%;padding:0 !important;height:0 !important}.vditor-menu--disabled svg{color:rgba(88,96,105,0.6);cursor:not-allowed}.vditor-emojis{display:inline-block;overflow:auto}.vditor-emojis::-webkit-scrollbar{display:none}.vditor-emojis__tip{flex:1;width:200px;margin-right:10px;color:#586069;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.vditor-emojis__tail{margin-top:5px;font-size:12px;color:#586069;display:flex}.vditor-emojis__tail a{text-decoration:none;color:#586069}.vditor-emojis__tail a:hover{color:#4285f4}.vditor-emojis span{cursor:pointer;border-radius:3px;float:left;height:30px;width:30px;text-align:center;line-height:26px;padding:3px;box-sizing:border-box;font-size:16px;transition:all 0.15s ease-in-out}.vditor-emojis span:hover{transform:scale(1.2)}.vditor-emojis img{height:20px;width:20px;float:left;margin:3px 0 0 3px}@keyframes slideInDown{from{transform:translate3d(0, -100%, 0);visibility:visible}to{transform:translate3d(0, 0, 0)}}.vditor{display:flex;flex-direction:column;border:1px solid #d1d5da;border-radius:3px;box-sizing:border-box;font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.vditor--fullscreen{position:fixed;top:0;width:100% !important;left:0;height:100vh !important;z-index:90;background-color:#fff}.vditor-content{display:flex;min-height:60px;flex:1;position:relative}.vditor-textarea{font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;margin:0;height:100%;overflow:auto;width:100%;flex:1;border:0;resize:none;padding:10px;box-sizing:border-box;background-color:#fafbfc;outline:0 none;font-size:16px;line-height:22px;color:#24292e;border-radius:0 0 3px 3px;font-variant-ligatures:no-common-ligatures;white-space:pre-wrap;word-break:break-word;word-wrap:break-word}.vditor-textarea[contenteditable="false"]{opacity:0.3;cursor:not-allowed}.vditor-textarea:focus{background-color:#fff}.vditor-textarea:empty::before{content:attr(placeholder);color:rgba(36,41,46,0.68)}.vditor-preview{flex:1;background-color:#fff;overflow:auto;padding:10px;box-shadow:inset 1px 0 #d1d5da;box-sizing:border-box;border-radius:0 0 3px 0}.vditor-preview>div::-webkit-scrollbar{display:none}.vditor-preview>.vditor-reset{margin:0 auto}.vditor-devtools{display:none;background-color:#fff;overflow:auto;flex:1;box-shadow:inset 1px 0 #d1d5da;box-sizing:border-box;border-radius:0 0 3px 0;padding:10px}.vditor-counter{padding:0 3px;position:absolute;bottom:10px;right:20px;color:#24292e;background-color:rgba(255,255,255,0.6);border-radius:3px;font-size:12px;user-select:none;z-index:2}.vditor-counter--error{color:#d23f31;background-color:rgba(210,63,49,0.1)}.vditor-resize{padding:3px 0;cursor:row-resize;user-select:none;position:absolute;width:100%;z-index:2}.vditor-resize--top{top:-3px}.vditor-resize--bottom{bottom:-3px}.vditor-resize>div{height:3px;background-color:#f6f8fa;transition:all 0.15s ease-in-out}.vditor-resize:hover>div,.vditor-resize--selected>div{background-color:#4285f4}.vditor-resize:hover svg,.vditor-resize--selected svg{color:#fff}.vditor-resize svg{fill:currentColor;stroke-width:0;stroke:currentColor;width:13px;height:3px;display:block;margin:0 auto;color:#586069}.vditor-upload{position:absolute;height:3px;left:0;top:-2px;transition:all 0.15s ease-in-out;background-color:rgba(66,133,244,0.8)}.vditor-tip{position:absolute;font-size:12px;top:10px;color:#fff;animation-duration:.15s;animation-fill-mode:both;width:100%;text-align:center}.vditor-tip--show{display:block;animation-name:slideInDown}.vditor-tip__content{text-align:left;display:inline-block;line-height:16px;padding:3px 10px;border-radius:3px;background:rgba(66,133,244,0.8);position:relative}.vditor-tip__content ul{margin:2px 0;padding:0 0 0 18px}.vditor-tip__close{position:absolute;color:#586069;top:-7px;right:-15px;font-weight:bold;cursor:pointer}.vditor-tip__close:hover{color:#4285f4}.vditor-wysiwyg{background-color:#fafbfc;box-sizing:border-box;flex:1;margin:0}.vditor-wysiwyg[contenteditable="false"]{opacity:0.3;cursor:not-allowed}.vditor-wysiwyg:focus{outline:none;background-color:#fff}.vditor-wysiwyg .marker{height:0;width:0;display:inline-block;overflow:hidden;transition:color .15s ease-in-out}.vditor-wysiwyg .newline{display:none}.vditor-wysiwyg .node--expand .marker{width:auto;height:auto;display:inline;color:#bbb}.vditor-wysiwyg p:empty:after{content:'\200b'}.vditor-wysiwyg li p{display:inline-block}.vditor-hint{background-color:#fff;position:absolute;box-shadow:0 1px 2px rgba(0,0,0,0.2);border-radius:3px;padding:5px 0;z-index:3;line-height:20px;list-style:none;color:#24292e;font-size:12px;margin:0;max-width:250px;min-width:80px;display:none}.vditor-hint li{cursor:pointer;padding:3px 10px;border-bottom:1px solid #d1d5da;line-height:20px}.vditor-hint li:last-child{border-bottom:0}.vditor-hint--current,.vditor-hint li:hover{background-color:#4285f4;color:#fff}.vditor-hint__emoji{font-size:16px;float:left;margin-right:3px}.vditor-hint img{height:20px;width:20px;float:left;margin-right:3px}.vditor-reset{font-variant-ligatures:no-common-ligatures;font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;word-wrap:break-word;overflow:auto;line-height:1.65;font-size:16px;word-break:break-word}.vditor-reset ul ul ul{list-style-type:square}.vditor-reset ul ul{list-style-type:circle}.vditor-reset ul{list-style-type:disc}.vditor-reset ul,.vditor-reset ol{padding-left:2em;margin-top:0;margin-bottom:16px}.vditor-reset li{margin-top:0.25em}.vditor-reset audio{max-width:100%}.vditor-reset audio:focus{outline:none}.vditor-reset video{max-height:90vh;max-width:100%}.vditor-reset img{max-width:100%}.vditor-reset img.emoji{cursor:auto;max-width:20px;vertical-align:sub}.vditor-reset h1,.vditor-reset h2,.vditor-reset h3,.vditor-reset h4,.vditor-reset h5,.vditor-reset h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.vditor-reset h1{padding-bottom:0.3em;font-size:1.7em;border-bottom:1px solid #eee}.vditor-reset h2{padding-bottom:0.3em;font-size:1.5em;border-bottom:1px solid #eee}.vditor-reset h3{font-size:1.25em}.vditor-reset h4{font-size:1em}.vditor-reset h5{font-size:0.875em}.vditor-reset h6{font-size:0.85em}.vditor-reset hr{height:2px;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.vditor-reset p{margin-top:0;margin-bottom:16px}.vditor-reset blockquote{padding:0 1em;color:#777;border-left:0.25em solid #ddd;margin:0 0 16px 0}.vditor-reset blockquote p{margin:0}.vditor-reset ins>iframe{border:0}.vditor-reset iframe{border:1px solid #d1d5da;max-width:100%;box-sizing:border-box}.vditor-reset iframe.iframe__video{min-width:80%;min-height:36vh}.vditor-reset table{width:100%;border:1px solid #dedede;margin:15px auto;border-collapse:collapse;empty-cells:show}.vditor-reset thead{text-align:center}.vditor-reset td,.vditor-reset th{height:35px;border:1px solid #dedede;padding:0 10px}.vditor-reset th{font-weight:bold;text-align:center !important;background:rgba(158,188,226,0.2)}.vditor-reset tbody tr:nth-child(2n){background:rgba(158,188,226,0.12)}.vditor-reset tr:hover{background:#efefef}.vditor-reset code:not(.hljs){padding:0.2em 0.4em;margin:0;font-size:85%;border-radius:3px;font-family:mononoki,Consolas,"Liberation Mono",Menlo,Courier,monospace;word-break:break-word;background-size:20px 20px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==);white-space:pre-wrap;background-color:rgba(27,31,35,0.05)}.vditor-reset pre>code{margin:0;font-size:85%;padding:0.5em;border-radius:5px;display:block;overflow:auto;white-space:pre;font-family:mononoki,Consolas,"Liberation Mono",Menlo,Courier,monospace;background-size:20px 20px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==)}.vditor-reset pre:hover div.vditor-copy{display:block}.vditor-reset kbd{display:inline-block;padding:3px 5px;font:11px Consolas, "Liberation Mono", Menlo, Courier, monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:solid 1px #d1d5da;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.vditor-reset summary{cursor:pointer}.vditor-reset summary:focus{outline:none}.vditor-reset svg{height:auto;width:auto}.vditor-reset p:last-child,.vditor-reset blockquote:last-child,.vditor-reset pre:last-child,.vditor-reset ul:last-child,.vditor-reset ol:last-child,.vditor-reset hr:last-child{margin-bottom:0}.vditor-reset .language-echarts{overflow:hidden;height:420px}.vditor-task{list-style:none;margin-left:-1.4em;display:flex;align-items:center}.vditor-task input{margin-right:5px}.vditor-copy{position:relative;display:none}.vditor-copy textarea{position:absolute;left:-100000px}.vditor-copy span{cursor:pointer;position:absolute;right:0.5em;top:0.5em;height:14px;width:14px;display:block;background-color:#f6f8fa;border-radius:3px;padding:3px}.vditor-copy svg{color:#586069;height:14px;width:14px !important;display:block;fill:currentColor}.vditor--error{color:#d23f31;font-size:12px;display:block;line-height:16px}textarea{overflow:hidden}button,.button{background:url(../images/icon.png) repeat center bottom;border:1px solid;border-color:#CCC #BBBBBB #A0A0A0;border-radius:4px;height:28px;margin:0;padding:0 6px;vertical-align:top}button:hover,.button:hover{border-color:#BBB}.padding12{padding:12px}.paddingBottom12{padding-bottom:12px !important}.paddingTop12{padding-top:12px !important}.margin12{margin:12px}.marginTop12{margin-top:12px !important}.marginBottom12{margin-bottom:12px !important}.marginLeft12{margin-left:12px !important}.marginLeft6{margin-left:6px !important}.marginRight12{margin-right:12px !important}.f-bold{font-weight:bold !important}.nowrap{white-space:nowrap}.left{float:left}.right{float:right}.clear{background-color:transparent;border:0;clear:both;display:block;font-size:0;height:0;line-height:0;overflow:hidden}.fn-clear:before,.fn-clear:after{display:table;content:""}.fn-clear:after{clear:both}.fn__flex{display:flex}.fn__flex1{flex:1;min-width:1px}.fn__flex-inline{display:inline-flex !important;align-items:center}.none{display:none}.pointer{cursor:pointer}.no-underline{text-decoration:none !important}.red{color:red}.contentBody{margin-bottom:12px;padding:3px 12px}.form th{text-align:right;white-space:nowrap}.form input[type='text'],.form input[type='password'],.form textarea{border-color:#C0C0C0 #D9D9D9 #D9D9D9;border-right:1px solid #D9D9D9;border-style:solid;border-width:1px;font-family:Helvetica,Arial,sans-serif;font-size:12px;outline:medium none;width:99%;padding:0 3px}.form input{height:24px}.form input[type='checkbox']{border:0;vertical-align:sub;height:20px}.form input.normalInput{width:auto}.form input[type='checkbox']:focus{border:0;box-shadow:0 0 0}.form input:focus,.form textarea:focus{box-shadow:0 1px 2px rgba(0,0,0,0.3) inset}.form textarea{padding:3px;overflow:auto;resize:vertical}.logo{padding:0 5px;text-decoration:none;text-shadow:0 0 1px #EEEEEE}.putTopIcon,.notPutTopIcon,.deleteIcon,.updateIcon,.commentIcon,.homeIcon,.adminIcon,.loginIcon,.logoutIcon,.calendarIcon,.browserIcon,.postIcon,.articlesIcon,.draftsIcon,.usersIcon,.linkIcon,.preferenceIcon,.pageIcon,.trueIcon,.falseIcon,.fileIcon,.othersIcon,.goTopIcon,.goBottomIcon,.cacheIcon,.aboutIcon{background-image:url("../images/icon.png");cursor:pointer;height:16px;width:16px}.adminIcon,.loginIcon,.logoutIcon,.homeIcon{margin-top:3px}.pageIcon{background-position:-144px 0px}.postIcon{background-position:-16px 0}.linkIcon{background-position:-32px 0}.preferenceIcon{background-position:-48px 0}.articlesIcon{background-position:-64px 0}.trueIcon{background-position:-80px 0;cursor:auto}.falseIcon{background-position:-96px 0;cursor:auto}.fileIcon{background-position:-112px 0}.deleteIcon{background-position:0 -16px}.updateIcon{background-position:-16px -16px}.commentIcon{background-position:-32px -16px}.homeIcon{background-position:-64px -16px}.adminIcon{background-position:-80px -16px}.logoutIcon{background-position:-96px -16px}.loginIcon{background-position:-112px -16px}.browserIcon{background-position:-128px -16px}.calendarIcon{background-position:-144px -16px}.putTopIcon{background-position:-16px -32px;float:left}.notPutTopIcon{background-position:-32px -32px;float:left}.othersIcon{background-position:-48px -32px}.cacheIcon{background-position:-96px -32px}.draftsIcon{background-position:-96px -32px}.usersIcon{background-position:-112px -32px}.goTopIcon{background-position:-64px -32px;position:fixed;right:20px;top:45%}.goBottomIcon{background-position:-80px -32px;bottom:40%;position:fixed;right:20px}#tags{list-style:none}#tags li{float:left;list-style:none;height:38px}#tags a:hover{text-shadow:0 0 2px #555555}#tags a{border-radius:3px 3px 3px 3px;box-shadow:1px 1px 3px #555555;float:left;margin:3px 6px;padding:3px 12px;text-decoration:none}#tags .tags1{font-size:12px;font-weight:normal}#tags .tags2{font-size:14px;font-weight:normal}#tags .tags3{font-size:16px;font-weight:normal}#tags .tags4{font-size:18px;font-weight:bold}#tags .tags5{font-size:20px;font-weight:bold}#captcha,#captchaReply{cursor:pointer} diff --git a/src/main/webapp/scss/base.scss b/src/main/webapp/scss/base.scss new file mode 100644 index 00000000..d56efb58 --- /dev/null +++ b/src/main/webapp/scss/base.scss @@ -0,0 +1,388 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + /** + * base style + * + * @author Liyuan Li + * @version 2.0.0.1, Mar 16, 2019 +*/ + +@import "../scss/reset"; +@import "vditor/src/assets/scss/classic"; + +textarea { + overflow: hidden; +} + +button, .button { + background: url(../images/icon.png) repeat center bottom; + border: 1px solid; + border-color: #CCC #BBBBBB #A0A0A0; + border-radius: 4px; + height: 28px; + margin: 0; + padding: 0 6px; + vertical-align: top; +} + +button:hover,.button:hover { + border-color: #BBB; +} + +.padding12 { + padding: 12px; +} + +.paddingBottom12 { + padding-bottom: 12px !important; +} + +.paddingTop12 { + padding-top: 12px !important; +} + +.margin12 { + margin: 12px; +} + +.marginTop12 { + margin-top: 12px !important; +} + +.marginBottom12 { + margin-bottom: 12px !important; +} + +.marginLeft12 { + margin-left: 12px !important; +} + +.marginLeft6 { + margin-left: 6px !important; +} + +.marginRight12 { + margin-right: 12px !important; +} + +.f-bold { + font-weight: bold !important; +} +.nowrap { + white-space: nowrap; +} + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + background-color: transparent; + border: 0; + clear: both; + display: block; + font-size: 0; + height: 0; + line-height: 0; + overflow: hidden; +} + +.fn-clear:before, +.fn-clear:after { + display: table; + content: ""; +} + +.fn-clear:after { + clear: both; +} + +.fn__flex { + display: flex; +} + +.fn__flex1 { + flex: 1; + min-width: 1px; +} + +.fn__flex-inline { + display: inline-flex !important; + align-items: center; +} + +.none { + display: none; +} + +.pointer { + cursor: pointer; +} + +.no-underline { + text-decoration: none !important; +} + +.red { + color: red; +} + +.contentBody { + margin-bottom: 12px; + padding: 3px 12px; +} + +/* start form */ +.form th { + text-align: right; + white-space: nowrap; +} + +.form input[type='text'], .form input[type='password'], .form textarea { + border-color: #C0C0C0 #D9D9D9 #D9D9D9; + border-right: 1px solid #D9D9D9; + border-style: solid; + border-width: 1px; + font-family: Helvetica,Arial,sans-serif; + font-size: 12px; + outline: medium none; + width: 99%; + padding: 0 3px; +} +.form input { + height: 24px; +} + +.form input[type='checkbox'] { + border: 0; + vertical-align: sub; + height: 20px; +} + +.form input.normalInput { + width: auto; +} + +.form input[type='checkbox']:focus { + border: 0; + box-shadow: 0 0 0; +} + +.form input:focus, .form textarea:focus { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) inset; +} + +.form textarea { + padding: 3px; + overflow: auto; + resize: vertical; +} +/* end form */ + +.logo { + padding: 0 5px; + text-decoration: none; + text-shadow: 0 0 1px #EEEEEE; +} + +/* start icon */ +.putTopIcon,.notPutTopIcon,.deleteIcon,.updateIcon,.commentIcon, +.homeIcon,.adminIcon,.loginIcon,.logoutIcon,.calendarIcon,.browserIcon, +.postIcon,.articlesIcon,.draftsIcon,.usersIcon,.linkIcon, +.preferenceIcon, .pageIcon, .trueIcon,.falseIcon, +.fileIcon, .othersIcon, .goTopIcon, .goBottomIcon, .cacheIcon, .aboutIcon { + background-image: url("../images/icon.png"); + cursor: pointer; + height: 16px; + width: 16px; +} + +.adminIcon,.loginIcon,.logoutIcon,.homeIcon { + margin-top: 3px; +} + +.pageIcon { + background-position: -144px 0px; +} + +.postIcon { + background-position: -16px 0; +} + +.linkIcon { + background-position: -32px 0; +} + +.preferenceIcon { + background-position: -48px 0; +} + +.articlesIcon { + background-position: -64px 0; +} + +.trueIcon { + background-position: -80px 0; + cursor: auto; +} + +.falseIcon { + background-position: -96px 0; + cursor: auto; +} + +.fileIcon { + background-position: -112px 0; +} + +.deleteIcon { + background-position: 0 -16px; +} + +.updateIcon { + background-position: -16px -16px; +} + +.commentIcon { + background-position: -32px -16px; +} + +.homeIcon { + background-position: -64px -16px; +} + +.adminIcon { + background-position: -80px -16px; +} + +.logoutIcon { + background-position: -96px -16px; +} + +.loginIcon { + background-position: -112px -16px; +} + +.browserIcon { + background-position: -128px -16px; +} + +.calendarIcon { + background-position: -144px -16px; +} + +.putTopIcon { + background-position: -16px -32px; + float: left; +} + +.notPutTopIcon { + background-position: -32px -32px; + float: left; +} + +.othersIcon { + background-position: -48px -32px; +} + +.cacheIcon { + background-position: -96px -32px; +} + +.draftsIcon { + background-position: -96px -32px; +} + +.usersIcon { + background-position: -112px -32px; +} + +.goTopIcon { + background-position: -64px -32px; + position: fixed; + right: 20px; + top: 45%; +} + +.goBottomIcon { + background-position: -80px -32px; + bottom: 40%; + position: fixed; + right: 20px; +} +/* end icon */ + +/* start tags */ +#tags { + list-style: none; +} + +#tags li { + float: left; + list-style: none; + height: 38px; +} + +#tags a:hover { + text-shadow: 0 0 2px #555555; +} + +#tags a { + border-radius: 3px 3px 3px 3px; + box-shadow: 1px 1px 3px #555555; + float: left; + margin: 3px 6px; + padding: 3px 12px; + text-decoration: none; +} + +#tags .tags1 { + font-size: 12px; + font-weight: normal; +} + +#tags .tags2 { + font-size: 14px; + font-weight: normal; +} + +#tags .tags3 { + font-size: 16px; + font-weight: normal; +} + +#tags .tags4 { + font-size: 18px; + font-weight: bold; +} + +#tags .tags5 { + font-size: 20px; + font-weight: bold; +} +/* end tags */ + +/* start comment */ +#captcha, #captchaReply { + cursor: pointer; +} +/* end comment */ \ No newline at end of file diff --git a/src/main/webapp/scss/fonts/icomoon.eot b/src/main/webapp/scss/fonts/icomoon.eot new file mode 100755 index 00000000..66aa7589 Binary files /dev/null and b/src/main/webapp/scss/fonts/icomoon.eot differ diff --git a/src/main/webapp/scss/fonts/icomoon.svg b/src/main/webapp/scss/fonts/icomoon.svg new file mode 100755 index 00000000..d9444d43 --- /dev/null +++ b/src/main/webapp/scss/fonts/icomoon.svg @@ -0,0 +1,21 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/scss/fonts/icomoon.ttf b/src/main/webapp/scss/fonts/icomoon.ttf new file mode 100755 index 00000000..c4096e5d Binary files /dev/null and b/src/main/webapp/scss/fonts/icomoon.ttf differ diff --git a/src/main/webapp/scss/fonts/icomoon.woff b/src/main/webapp/scss/fonts/icomoon.woff new file mode 100755 index 00000000..8bf77d07 Binary files /dev/null and b/src/main/webapp/scss/fonts/icomoon.woff differ diff --git a/src/main/webapp/scss/fonts/selection.json b/src/main/webapp/scss/fonts/selection.json new file mode 100755 index 00000000..515e9da2 --- /dev/null +++ b/src/main/webapp/scss/fonts/selection.json @@ -0,0 +1 @@ +{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M1002.667 120.124l-391.876 391.876 391.876 391.876-98.791 98.791-391.876-391.876-391.876 391.876-98.791-98.791 391.876-391.876-391.876-391.876 98.791-98.791 391.876 391.876 391.876-391.876z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["close"],"grid":16,"defaultCode":59657},"attrs":[],"properties":{"order":71,"id":52,"prevSize":32,"name":"close","code":59657},"setIdx":0,"setId":1,"iconIdx":0},{"icon":{"paths":["M832 320h-192v-320h-256v320h-192l320 384 320-384zM192 1024h640v-192h-640v192z"],"width":1024,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["move-down"],"grid":16,"defaultCode":59658},"attrs":[{}],"properties":{"order":72,"id":53,"name":"move-down","prevSize":32,"code":59658},"setIdx":0,"setId":1,"iconIdx":1},{"icon":{"paths":["M192 704h192v320h256v-320h192l-320-384-320 384zM192 0v192h640v-192h-640z"],"width":1024,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["move-up"],"grid":16,"defaultCode":59659},"attrs":[{}],"properties":{"order":73,"id":54,"name":"move-up","prevSize":32,"code":59659},"setIdx":0,"setId":1,"iconIdx":2},{"icon":{"paths":["M384 64h640v128h-640v-128zM384 448h640v128h-640v-128zM384 832h640v128h-640v-128zM0 128c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM0 512c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM0 896c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"defaultCode":57348,"grid":16},"attrs":[],"properties":{"id":55,"order":74,"ligatures":"","prevSize":32,"name":"unordered-list","code":57348},"setIdx":0,"setId":1,"iconIdx":3},{"icon":{"paths":["M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416zM448 256h128v128h-128zM640 768h-256v-64h64v-192h-64v-64h192v256h64z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"defaultCode":57357,"grid":16},"attrs":[],"properties":{"id":56,"order":75,"ligatures":"","prevSize":32,"name":"info","code":57357},"setIdx":0,"setId":1,"iconIdx":4},{"icon":{"paths":["M928 200h-832v-104h832zM616 408h-520v-104h520zM928 720h-832v-104h832zM720 928h-624v-104h624zM928 876c0 28.73-23.322 52-51.948 52-28.808 0-52.052-23.27-52.052-52s23.244-52 52.052-52c28.626 0 51.948 23.27 51.948 52z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["article"],"grid":16,"defaultCode":58909},"attrs":[],"properties":{"order":76,"id":57,"prevSize":32,"name":"article","code":58909},"setIdx":0,"setId":1,"iconIdx":5},{"icon":{"paths":["M929.254 614.698c-53.148-92.054-21.186-210.021 71.377-263.622l-99.544-172.418c-28.441 16.673-61.507 26.233-96.794 26.233-106.379 0-192.614-86.803-192.614-193.883h-199.094c0.263 33.051-7.988 66.559-25.678 97.195-53.147 92.056-171.294 123.361-263.993 69.997l-99.542 172.418c28.659 16.295 53.469 40.152 71.113 70.71 53.061 91.909 21.281 209.641-70.937 263.361l99.544 172.418c28.342-16.516 61.25-25.972 96.353-25.972 106.043 0 192.057 86.254 192.607 192.862h199.090c-0.085-32.718 8.183-65.854 25.687-96.172 53.059-91.907 170.918-123.255 263.549-70.248l99.544-172.416c-28.471-16.289-53.115-40.063-70.666-70.462zM512 722.607c-113.277 0-205.107-91.826-205.107-205.105 0-113.277 91.828-205.105 205.107-205.105 113.277 0 205.102 91.828 205.102 205.105-0.002 113.279-91.826 205.105-205.102 205.105z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["setting"],"defaultCode":58884,"grid":16,"colorPermutations":{}},"attrs":[],"properties":{"id":58,"order":77,"prevSize":32,"name":"setting","code":58884},"setIdx":0,"setId":1,"iconIdx":6},{"icon":{"paths":["M305.92 650.346v-304.213h-186.453c-53.973 0-98.133 44.16-98.133 98.133v294.4c0 53.973 44.16 98.133 98.133 98.133h49.067v147.2l147.2-147.2h245.333c53.973 0 98.133-44.16 98.133-98.133v-89.301c-3.141 0.686-6.476 1.031-9.813 1.031h-343.467zM904.533 100.799h-441.6c-53.973 0-98.133 44.16-98.133 98.133v392.533h343.467l147.2 147.2v-147.2h49.067c53.973 0 98.133-44.16 98.133-98.133v-294.4c0-53.973-44.16-98.133-98.133-98.133z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["cmts"],"defaultCode":58890,"grid":16,"colorPermutations":{}},"attrs":[],"properties":{"id":59,"order":78,"prevSize":32,"name":"cmts","code":58890},"setIdx":0,"setId":1,"iconIdx":7},{"icon":{"paths":["M843.945 208.18c-82.273-89.839-200.526-146.18-331.945-146.18-248.528 0-450 201.472-450 450h84.375c0-201.929 163.696-365.625 365.625-365.625 108.123 0 205.262 46.949 272.194 121.558l-131.569 131.567h309.375v-309.375l-118.055 118.055zM877.625 512c0 201.929-163.696 365.625-365.625 365.625-108.123 0-205.262-46.949-272.192-121.558l131.567-131.567h-309.375v309.375l118.055-118.055c82.272 89.839 200.526 146.18 331.945 146.18 248.528 0 450-201.472 450-450h-84.375z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["refresh"],"defaultCode":58891,"grid":16,"colorPermutations":{}},"attrs":[],"properties":{"id":60,"order":79,"prevSize":32,"name":"refresh","code":58891},"setIdx":0,"setId":1,"iconIdx":8},{"icon":{"paths":["M512 320l-192 192-192-192-128 128 320 320 320-320-128-128z"],"width":640,"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["chevron-down"],"defaultCode":58904,"grid":16,"colorPermutations":{}},"attrs":[],"properties":{"id":61,"order":80,"prevSize":32,"name":"chevron-down","code":58904},"setIdx":0,"setId":1,"iconIdx":9},{"icon":{"paths":["M320 256l-320 320 128 128 192-192 192 192 128-128-320-320z"],"width":640,"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["chevron-up"],"defaultCode":58905,"grid":16,"colorPermutations":{}},"attrs":[],"properties":{"id":62,"order":81,"prevSize":32,"name":"chevron-up","code":58905},"setIdx":0,"setId":1,"iconIdx":10}],"height":1024,"metadata":{"name":"icomoon"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icomoon"},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":4473924,"bgColor":16777215,"name":"icomoon","classSelector":".icon"},"historySize":100,"showCodes":false,"gridSize":16,"showGrid":true}} \ No newline at end of file diff --git a/src/main/webapp/scss/start.css b/src/main/webapp/scss/start.css new file mode 100644 index 00000000..2e525328 --- /dev/null +++ b/src/main/webapp/scss/start.css @@ -0,0 +1 @@ +html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;height:100%}body{margin:0;font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;font-size:14px;background-color:#fff;-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch}::-moz-selection{text-shadow:none;background:rgba(65,131,196,0.4)}::selection{text-shadow:none;background:rgba(66,133,244,0.4)}ul,ol{margin:0;padding:0}h1,h2,h3,h4,h5,h6,dl,dd,p{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none}a{outline:0;text-decoration:none}a:hover{text-decoration:underline}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{max-width:100%;vertical-align:middle;border:0;height:auto;-ms-interpolation-mode:bicubic;overflow:hidden;font-size:12px}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;font-family:"Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;outline:none}button,input{line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;resize:vertical}svg{fill:currentColor;display:inline-block;stroke-width:0;stroke:currentColor;width:14px;height:14px}blockquote{margin:0}@keyframes tooltip-appear{from{opacity:0}to{opacity:1}}.vditor-tooltipped{position:relative;cursor:pointer}.vditor-tooltipped::after{position:absolute;z-index:1000000;display:none;padding:5px 8px;font-size:11px;font-weight:normal;-webkit-font-smoothing:subpixel-antialiased;color:#fff;text-align:center;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:rgba(0,0,0,0.8);border-radius:3px;line-height:16px;opacity:0}.vditor-tooltipped::before{position:absolute;z-index:1000001;display:none;width:0;height:0;color:rgba(0,0,0,0.8);pointer-events:none;content:"";border:5px solid transparent;opacity:0}.vditor-tooltipped--hover::before,.vditor-tooltipped--hover::after,.vditor-tooltipped:hover::before,.vditor-tooltipped:hover::after,.vditor-tooltipped:active::before,.vditor-tooltipped:active::after,.vditor-tooltipped:focus::before,.vditor-tooltipped:focus::after{display:inline-block;text-decoration:none;animation-name:tooltip-appear;animation-duration:0.15s;animation-fill-mode:forwards;animation-timing-function:ease-in}.vditor-tooltipped__s::after,.vditor-tooltipped__se::after,.vditor-tooltipped__sw::after{top:100%;right:50%;margin-top:5px}.vditor-tooltipped__s::before,.vditor-tooltipped__se::before,.vditor-tooltipped__sw::before{top:auto;right:50%;bottom:-5px;margin-right:-5px;border-bottom-color:rgba(0,0,0,0.8)}.vditor-tooltipped__se::after{right:auto;left:50%;margin-left:-15px}.vditor-tooltipped__sw::after{margin-right:-15px}.vditor-tooltipped__n::after,.vditor-tooltipped__ne::after,.vditor-tooltipped__nw::after{right:50%;bottom:100%;margin-bottom:5px}.vditor-tooltipped__n::before,.vditor-tooltipped__ne::before,.vditor-tooltipped__nw::before{top:-5px;right:50%;bottom:auto;margin-right:-5px;border-top-color:rgba(0,0,0,0.8)}.vditor-tooltipped__ne::after{right:auto;left:50%;margin-left:-15px}.vditor-tooltipped__nw::after{margin-right:-15px}.vditor-tooltipped__s::after,.vditor-tooltipped__n::after{transform:translateX(50%)}.vditor-tooltipped__w::after{right:100%;bottom:50%;margin-right:5px;transform:translateY(50%)}.vditor-tooltipped__w::before{top:50%;bottom:50%;left:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,0.8)}.vditor-tooltipped__e::after{bottom:50%;left:100%;margin-left:5px;transform:translateY(50%)}.vditor-tooltipped__e::before{top:50%;right:-5px;bottom:50%;margin-top:-5px;border-right-color:rgba(0,0,0,0.8)}@keyframes scale-in{0%{opacity:0;transform:scale(0.5)}100%{opacity:1;transform:scale(1)}}.vditor-panel{background-color:#fff;position:absolute;box-shadow:0 1px 2px rgba(0,0,0,0.2);border-radius:3px;padding:5px;z-index:4;font-size:14px;display:none;user-select:none;max-width:320px;min-width:80px;animation-duration:.15s;animation-name:scale-in;animation-timing-function:cubic-bezier(0.2, 0, 0.13, 1.5)}.vditor-panel h1,.vditor-panel h2,.vditor-panel h3,.vditor-panel h4,.vditor-panel h5,.vditor-panel h6{margin:0;cursor:pointer;padding:3px 10px;border-radius:3px;line-height:normal}.vditor-panel h1:hover,.vditor-panel h2:hover,.vditor-panel h3:hover,.vditor-panel h4:hover,.vditor-panel h5:hover,.vditor-panel h6:hover{background-color:#4285f4;color:#fff}.vditor-toolbar{background-color:#f6f8fa;border-bottom:1px solid #d1d5da;padding:0 5px;border-radius:3px 3px 0 0}.vditor-toolbar>div{padding:10px 5px;float:left;line-height:14px;height:36px;box-sizing:border-box}.vditor-toolbar svg{fill:currentColor;display:inline-block;stroke-width:0;stroke:currentColor;width:14px;height:14px}.vditor-toolbar .vditor-tooltipped{color:#586069}.vditor-toolbar .vditor-tooltipped:hover{color:#4285f4}.vditor-toolbar label{overflow:hidden;position:relative;height:14px;width:15px;display:block;cursor:pointer}.vditor-toolbar label svg{position:absolute;top:0;left:0}.vditor-toolbar label input{position:absolute;width:15px;height:15px;top:0;left:0;opacity:.001;overflow:hidden}.vditor-menu--current svg{color:#4285f4}.vditor-menu__divider{width:10px}.vditor-menu__br{width:100%;padding:0 !important;height:0 !important}.vditor-menu--disabled svg{color:rgba(88,96,105,0.6);cursor:not-allowed}.vditor-emojis{display:inline-block;overflow:auto}.vditor-emojis::-webkit-scrollbar{display:none}.vditor-emojis__tip{flex:1;width:200px;margin-right:10px;color:#586069;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.vditor-emojis__tail{margin-top:5px;font-size:12px;color:#586069;display:flex}.vditor-emojis__tail a{text-decoration:none;color:#586069}.vditor-emojis__tail a:hover{color:#4285f4}.vditor-emojis span{cursor:pointer;border-radius:3px;float:left;height:30px;width:30px;text-align:center;line-height:26px;padding:3px;box-sizing:border-box;font-size:16px;transition:all 0.15s ease-in-out}.vditor-emojis span:hover{transform:scale(1.2)}.vditor-emojis img{height:20px;width:20px;float:left;margin:3px 0 0 3px}@keyframes slideInDown{from{transform:translate3d(0, -100%, 0);visibility:visible}to{transform:translate3d(0, 0, 0)}}.vditor{display:flex;flex-direction:column;border:1px solid #d1d5da;border-radius:3px;box-sizing:border-box;font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.vditor--fullscreen{position:fixed;top:0;width:100% !important;left:0;height:100vh !important;z-index:90;background-color:#fff}.vditor-content{display:flex;min-height:60px;flex:1;position:relative}.vditor-textarea{font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;margin:0;height:100%;overflow:auto;width:100%;flex:1;border:0;resize:none;padding:10px;box-sizing:border-box;background-color:#fafbfc;outline:0 none;font-size:16px;line-height:22px;color:#24292e;border-radius:0 0 3px 3px;font-variant-ligatures:no-common-ligatures;white-space:pre-wrap;word-break:break-word;word-wrap:break-word}.vditor-textarea[contenteditable="false"]{opacity:0.3;cursor:not-allowed}.vditor-textarea:focus{background-color:#fff}.vditor-textarea:empty::before{content:attr(placeholder);color:rgba(36,41,46,0.68)}.vditor-preview{flex:1;background-color:#fff;overflow:auto;padding:10px;box-shadow:inset 1px 0 #d1d5da;box-sizing:border-box;border-radius:0 0 3px 0}.vditor-preview>div::-webkit-scrollbar{display:none}.vditor-preview>.vditor-reset{margin:0 auto}.vditor-devtools{display:none;background-color:#fff;overflow:auto;flex:1;box-shadow:inset 1px 0 #d1d5da;box-sizing:border-box;border-radius:0 0 3px 0;padding:10px}.vditor-counter{padding:0 3px;position:absolute;bottom:10px;right:20px;color:#24292e;background-color:rgba(255,255,255,0.6);border-radius:3px;font-size:12px;user-select:none;z-index:2}.vditor-counter--error{color:#d23f31;background-color:rgba(210,63,49,0.1)}.vditor-resize{padding:3px 0;cursor:row-resize;user-select:none;position:absolute;width:100%;z-index:2}.vditor-resize--top{top:-3px}.vditor-resize--bottom{bottom:-3px}.vditor-resize>div{height:3px;background-color:#f6f8fa;transition:all 0.15s ease-in-out}.vditor-resize:hover>div,.vditor-resize--selected>div{background-color:#4285f4}.vditor-resize:hover svg,.vditor-resize--selected svg{color:#fff}.vditor-resize svg{fill:currentColor;stroke-width:0;stroke:currentColor;width:13px;height:3px;display:block;margin:0 auto;color:#586069}.vditor-upload{position:absolute;height:3px;left:0;top:-2px;transition:all 0.15s ease-in-out;background-color:rgba(66,133,244,0.8)}.vditor-tip{position:absolute;font-size:12px;top:10px;color:#fff;animation-duration:.15s;animation-fill-mode:both;width:100%;text-align:center}.vditor-tip--show{display:block;animation-name:slideInDown}.vditor-tip__content{text-align:left;display:inline-block;line-height:16px;padding:3px 10px;border-radius:3px;background:rgba(66,133,244,0.8);position:relative}.vditor-tip__content ul{margin:2px 0;padding:0 0 0 18px}.vditor-tip__close{position:absolute;color:#586069;top:-7px;right:-15px;font-weight:bold;cursor:pointer}.vditor-tip__close:hover{color:#4285f4}.vditor-wysiwyg{background-color:#fafbfc;box-sizing:border-box;flex:1;margin:0}.vditor-wysiwyg[contenteditable="false"]{opacity:0.3;cursor:not-allowed}.vditor-wysiwyg:focus{outline:none;background-color:#fff}.vditor-wysiwyg .marker{height:0;width:0;display:inline-block;overflow:hidden;transition:color .15s ease-in-out}.vditor-wysiwyg .newline{display:none}.vditor-wysiwyg .node--expand .marker{width:auto;height:auto;display:inline;color:#bbb}.vditor-wysiwyg p:empty:after{content:'\200b'}.vditor-wysiwyg li p{display:inline-block}.vditor-hint{background-color:#fff;position:absolute;box-shadow:0 1px 2px rgba(0,0,0,0.2);border-radius:3px;padding:5px 0;z-index:3;line-height:20px;list-style:none;color:#24292e;font-size:12px;margin:0;max-width:250px;min-width:80px;display:none}.vditor-hint li{cursor:pointer;padding:3px 10px;border-bottom:1px solid #d1d5da;line-height:20px}.vditor-hint li:last-child{border-bottom:0}.vditor-hint--current,.vditor-hint li:hover{background-color:#4285f4;color:#fff}.vditor-hint__emoji{font-size:16px;float:left;margin-right:3px}.vditor-hint img{height:20px;width:20px;float:left;margin-right:3px}.vditor-reset{font-variant-ligatures:no-common-ligatures;font-family:"Helvetica Neue","Luxi Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei",sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;word-wrap:break-word;overflow:auto;line-height:1.65;font-size:16px;word-break:break-word}.vditor-reset ul ul ul{list-style-type:square}.vditor-reset ul ul{list-style-type:circle}.vditor-reset ul{list-style-type:disc}.vditor-reset ul,.vditor-reset ol{padding-left:2em;margin-top:0;margin-bottom:16px}.vditor-reset li{margin-top:0.25em}.vditor-reset audio{max-width:100%}.vditor-reset audio:focus{outline:none}.vditor-reset video{max-height:90vh;max-width:100%}.vditor-reset img{max-width:100%}.vditor-reset img.emoji{cursor:auto;max-width:20px;vertical-align:sub}.vditor-reset h1,.vditor-reset h2,.vditor-reset h3,.vditor-reset h4,.vditor-reset h5,.vditor-reset h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.vditor-reset h1{padding-bottom:0.3em;font-size:1.7em;border-bottom:1px solid #eee}.vditor-reset h2{padding-bottom:0.3em;font-size:1.5em;border-bottom:1px solid #eee}.vditor-reset h3{font-size:1.25em}.vditor-reset h4{font-size:1em}.vditor-reset h5{font-size:0.875em}.vditor-reset h6{font-size:0.85em}.vditor-reset hr{height:2px;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.vditor-reset p{margin-top:0;margin-bottom:16px}.vditor-reset blockquote{padding:0 1em;color:#777;border-left:0.25em solid #ddd;margin:0 0 16px 0}.vditor-reset blockquote p{margin:0}.vditor-reset ins>iframe{border:0}.vditor-reset iframe{border:1px solid #d1d5da;max-width:100%;box-sizing:border-box}.vditor-reset iframe.iframe__video{min-width:80%;min-height:36vh}.vditor-reset table{width:100%;border:1px solid #dedede;margin:15px auto;border-collapse:collapse;empty-cells:show}.vditor-reset thead{text-align:center}.vditor-reset td,.vditor-reset th{height:35px;border:1px solid #dedede;padding:0 10px}.vditor-reset th{font-weight:bold;text-align:center !important;background:rgba(158,188,226,0.2)}.vditor-reset tbody tr:nth-child(2n){background:rgba(158,188,226,0.12)}.vditor-reset tr:hover{background:#efefef}.vditor-reset code:not(.hljs){padding:0.2em 0.4em;margin:0;font-size:85%;border-radius:3px;font-family:mononoki,Consolas,"Liberation Mono",Menlo,Courier,monospace;word-break:break-word;background-size:20px 20px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==);white-space:pre-wrap;background-color:rgba(27,31,35,0.05)}.vditor-reset pre>code{margin:0;font-size:85%;padding:0.5em;border-radius:5px;display:block;overflow:auto;white-space:pre;font-family:mononoki,Consolas,"Liberation Mono",Menlo,Courier,monospace;background-size:20px 20px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==)}.vditor-reset pre:hover div.vditor-copy{display:block}.vditor-reset kbd{display:inline-block;padding:3px 5px;font:11px Consolas, "Liberation Mono", Menlo, Courier, monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:solid 1px #d1d5da;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.vditor-reset summary{cursor:pointer}.vditor-reset summary:focus{outline:none}.vditor-reset svg{height:auto;width:auto}.vditor-reset p:last-child,.vditor-reset blockquote:last-child,.vditor-reset pre:last-child,.vditor-reset ul:last-child,.vditor-reset ol:last-child,.vditor-reset hr:last-child{margin-bottom:0}.vditor-reset .language-echarts{overflow:hidden;height:420px}.vditor-task{list-style:none;margin-left:-1.4em;display:flex;align-items:center}.vditor-task input{margin-right:5px}.vditor-copy{position:relative;display:none}.vditor-copy textarea{position:absolute;left:-100000px}.vditor-copy span{cursor:pointer;position:absolute;right:0.5em;top:0.5em;height:14px;width:14px;display:block;background-color:#f6f8fa;border-radius:3px;padding:3px}.vditor-copy svg{color:#586069;height:14px;width:14px !important;display:block;fill:currentColor}.vditor--error{color:#d23f31;font-size:12px;display:block;line-height:16px}button{background-size:110% 110%;border:1px solid rgba(27,31,35,0.2);font-size:13px;font-weight:700;text-shadow:0 1px 0 rgba(255,255,255,0.9);white-space:nowrap;background-color:#eff3f6;background-image:linear-gradient(-180deg, #fafbfc, #eff3f6 90%);color:#24292e;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.075);outline:none;padding:7px 15px;margin-top:10px;cursor:pointer}button:hover{background-color:#e6ebf1;background-image:linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);background-position:-.5em;border-color:rgba(27,31,35,0.35)}a{color:#4285f4;text-decoration:none}h2{background-color:#F3F1E5;border-radius:4px 4px 0 0;font-size:16px;margin:0;padding:10px 20px}h2 a:hover{color:#d23f31}input[type=password],input[type=text]{border:1px solid #d1d5da;background-color:#FAFAFA;border-radius:3px;box-shadow:inset 0 1px 2px rgba(27,31,35,0.075);padding:7px 8px;width:100%;line-height:17px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;outline:none;margin-top:10px}input[type=password]:focus,input[type=text]:focus{background-color:#FFF;box-shadow:inset 0 1px 2px rgba(27,31,35,0.075),0 0 0 0.2em #dbedff;border:1px solid #4285f4}.error{color:#d23f31;font-weight:bold}.wrap{border-top:5px solid #E6E5D9;background-color:#F3F1E5;display:flex;flex-direction:column;min-height:100vh;box-sizing:border-box}.content-wrap{flex:1;display:flex}.content{background-color:#fff;margin:0 auto;width:760px;align-self:center;display:flex}.main{border-left:1px solid #E6E5D9;font-size:15px;padding:20px;flex:1}.footerWrapper{background-color:#FFFFFF;border-top:1px solid #E6E5D9;padding:12px 0;text-align:center}.a-error{text-align:right}.img-error{max-width:100%;margin:20px auto;padding:0;display:block}.kill__btns{text-align:right}.kill__img{float:right;margin:-280px 0 0 0}.kill .vditor-reset{margin:10px}#github{position:relative;text-align:center}#github.github--loading:after{content:"Loading...";position:absolute;height:100%;width:100%;top:0;left:0;z-index:7;background-color:rgba(255,255,255,0.6);font-size:22px;text-align:center;padding-top:135px;color:#000;text-shadow:0 1px 2px rgba(0,0,0,0.2);box-sizing:border-box}#github .github__icon{height:200px;width:266px;background-image:url("../images/github.png");margin:0 auto;background-size:contain;cursor:pointer}#github .github__icon:hover{background-image:url("../images/github.gif")}#github img{display:none}.github__text{cursor:pointer;font-size:12px;color:#666}.github__text ul{display:none;text-align:left;margin:5px auto 0;width:280px;padding-left:24px;box-sizing:border-box}.github__link{font-size:12px;text-decoration:underline;margin-top:10px;display:block}.github__check{font-size:12px;display:inline-block;margin:10px auto 0;align-items:center;line-height:20px;color:#999}.github__check a{color:#999;text-decoration:underline}.github__check input{width:auto;margin-right:5px}.search{border-top:5px solid #e6e5d9;background-color:#f3f1e5;padding:20px 0 20px 84px;min-height:480px;flex:1}.search__body{display:flex;flex-direction:column}.search__header{margin:20px;display:flex}.search__header a{line-height:32px}.search__input{flex:1;display:flex}.search__input input{margin:0 10px 0 20px;width:523px;height:32px;line-height:32px}.search__input button{height:32px;margin:0}.search__articles{width:600px}.search__articles .tag{color:#616161;padding:2px 5px;border:1px solid transparent;position:relative;height:20px;white-space:nowrap;word-wrap:normal;background-color:#f7f7f7;border-radius:3px 3px 3px 3px;line-height:21px;font-size:12px;display:inline-block;margin-right:5px}.search__articles .tag:hover{color:#000;box-shadow:0 1px 2px rgba(0,0,0,0.2);text-decoration:none}.search__articles header h1{margin:0}.search__articles header h1>a{font-size:18px;text-decoration:none;font-weight:normal}.search__articles .meta{font-size:14px;color:#999;margin:5px 0}.search__articles footer{margin-bottom:30px}.search__articles footer a{text-decoration:none;font-size:13px}.search__articles footer a:hover,.search__articles header h1>a:hover,.search__pagination a:hover{text-decoration:underline}.search__pagination a{color:#4285f4;text-decoration:none}.search__pagination a,.search__pagination span{margin-right:5px}@media (max-width: 780px){.wrap{padding:40px 0 0 0}.content{width:auto}.logo,.search__header img,.search__header>.fn-right{display:none}.main{border:0;float:none;width:auto;padding:10px}.a-500,.a-403,.a-404{margin:0 20px 0 0}#init{width:100%;padding:0 20px;box-sizing:border-box;left:0}.search{padding:20px;width:100%;min-height:auto;box-sizing:border-box}.search__input input{width:auto;flex:1;margin-left:0}.search__articles{width:auto}} diff --git a/src/main/webapp/scss/start.scss b/src/main/webapp/scss/start.scss new file mode 100644 index 00000000..947a6f4e --- /dev/null +++ b/src/main/webapp/scss/start.scss @@ -0,0 +1,406 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** +* 401, 403, 404, 500, article-pwd, start, search and kill-browser page style. +* +* @author Liyuan Li +* @author Liang Ding +* @version 2.0.0.4, Mar 17, 2019 +*/ + +@import "reset"; +@import "vditor/src/assets/scss/classic"; + +/* reset */ +button { + background-size: 110% 110%; + border: 1px solid rgba(27, 31, 35, .2); + font-size: 13px; + font-weight: 700; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.9); + white-space: nowrap; + background-color: #eff3f6; + background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%); + color: #24292e; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.075); + outline: none; + padding: 7px 15px; + margin-top: 10px; + cursor: pointer; +} + +button:hover { + background-color: #e6ebf1; + background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%); + background-position: -.5em; + border-color: rgba(27, 31, 35, .35); +} + +a { + color: #4285f4; + text-decoration: none; +} + +h2 { + background-color: #F3F1E5; + border-radius: 4px 4px 0 0; + font-size: 16px; + margin: 0; + padding: 10px 20px; + a:hover { + color: #d23f31; + } +} + +input[type=password], +input[type=text] { + border: 1px solid #d1d5da; + background-color: #FAFAFA; + border-radius: 3px; + box-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075); + padding: 7px 8px; + width: 100%; + line-height: 17px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + outline: none; + margin-top: 10px; +} + +input[type=password]:focus, +input[type=text]:focus { + background-color: #FFF; + box-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075), 0 0 0 0.2em #dbedff; + border: 1px solid #4285f4; +} + +/* common */ +.error { + color: #d23f31; + font-weight: bold; +} + +/* framework */ +.wrap { + border-top: 5px solid #E6E5D9; + background-color: #F3F1E5; + display: flex; + flex-direction: column; + min-height: 100vh; + box-sizing: border-box; +} + +.content-wrap { + flex: 1; + display: flex; +} + +.content { + background-color: #fff; + margin: 0 auto; + width: 760px; + align-self: center; + display: flex; +} + +.main { + border-left: 1px solid #E6E5D9; + font-size: 15px; + padding: 20px; + flex: 1; +} + +.footerWrapper { + background-color: #FFFFFF; + border-top: 1px solid #E6E5D9; + padding: 12px 0; + text-align: center; +} + +/* start 403/404/500 */ +.a-error { + text-align: right; +} + +.img-error { + max-width: 100%; + margin: 20px auto; + padding: 0; + display: block; +} + +/* end 403/404/500 */ + +/* start kill */ +.kill { + &__btns { + text-align: right; + } + &__img { + float: right; + margin: -280px 0 0 0; + } + .vditor-reset { + margin: 10px; + } +} +/* end kill */ + +/* start */ +#github { + position: relative; + text-align: center; +} + +#github.github--loading:after { + content: "Loading..."; + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 7; + background-color: rgba(255, 255, 255, 0.6); + font-size: 22px; + text-align: center; + padding-top: 135px; + color: #000; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; +} + +#github .github__icon { + height: 200px; + width: 266px; + background-image: url("../images/github.png"); + margin: 0 auto; + background-size: contain; + cursor: pointer; +} + +#github .github__icon:hover { + background-image: url("../images/github.gif"); +} + +#github img { + display: none; +} + +.github__text { + cursor: pointer; + font-size: 12px; + color: #666; +} + +.github__text ul { + display: none; + text-align: left; + margin: 5px auto 0; + width: 280px; + padding-left: 24px; + box-sizing: border-box; +} + +.github__link { + font-size: 12px; + text-decoration: underline; + margin-top: 10px; + display: block; +} + +.github__check { + font-size: 12px; + display: inline-block; + margin: 10px auto 0; + align-items: center; + line-height: 20px; + color: #999; + a { + color: #999; + text-decoration: underline; + } + input { + width: auto; + margin-right: 5px; + } +} + +/* start search */ +.search { + border-top: 5px solid #e6e5d9; + background-color: #f3f1e5; + padding: 20px 0 20px 84px; + min-height: 480px; + flex: 1; +} + +.search__body { + display: flex; + flex-direction: column; +} + +.search__header { + margin: 20px; + display: flex; +} + +.search__header a { + line-height: 32px; +} + +.search__input { + flex: 1; + display: flex; +} + +.search__input input { + margin: 0 10px 0 20px; + width: 523px; + height: 32px; + line-height: 32px; +} + +.search__input button { + height: 32px; + margin: 0; +} + +.search__articles { + width: 600px; + .tag { + color: #616161; + padding: 2px 5px; + border: 1px solid transparent; + position: relative; + height: 20px; + white-space: nowrap; + word-wrap: normal; + background-color: #f7f7f7; + border-radius: 3px 3px 3px 3px; + line-height: 21px; + font-size: 12px; + display: inline-block; + margin-right: 5px; + &:hover { + color: #000; + box-shadow: 0 1px 2px rgba(0,0,0,.2); + text-decoration: none; + } + } +} + +.search__articles header h1 { + margin: 0; +} + +.search__articles header h1 > a { + font-size: 18px; + text-decoration: none; + font-weight: normal; +} + +.search__articles .meta { + font-size: 14px; + color: #999; + margin: 5px 0; +} + +.search__articles footer { + margin-bottom: 30px; +} + +.search__articles footer a { + text-decoration: none; + font-size: 13px; +} + +.search__articles footer a:hover, +.search__articles header h1 > a:hover, +.search__pagination a:hover { + text-decoration: underline; +} + +.search__pagination a { + color: #4285f4; + text-decoration: none; +} + +.search__pagination a, +.search__pagination span { + margin-right: 5px; +} + +/* end search */ + +/* start responsive */ +@media (max-width: 780px) { + .wrap { + padding: 40px 0 0 0; + } + + .content { + width: auto; + } + + .logo, + .search__header img, + .search__header > .fn-right { + display: none; + } + + .main { + border: 0; + float: none; + width: auto; + padding: 10px; + } + + .a-500, + .a-403, + .a-404 { + margin: 0 20px 0 0; + } + + #init { + width: 100%; + padding: 0 20px; + box-sizing: border-box; + left: 0; + } + + .search { + padding: 20px; + width: 100%; + min-height: auto; + box-sizing: border-box; + + &__input input { + width: auto; + flex: 1; + margin-left: 0; + } + + &__articles { + width: auto; + } + } +} + +/* end responsive */ \ No newline at end of file diff --git a/src/main/webapp/skins b/src/main/webapp/skins new file mode 160000 index 00000000..72f9d681 --- /dev/null +++ b/src/main/webapp/skins @@ -0,0 +1 @@ +Subproject commit 72f9d681fd7cffe52b71a25ea13efc9992f451f8 diff --git a/src/main/webapp/sw.js b/src/main/webapp/sw.js new file mode 100644 index 00000000..6768534b --- /dev/null +++ b/src/main/webapp/sw.js @@ -0,0 +1,27 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @fileoverview 注册 Service Work,目前为空,只为触发添加到桌面。 + * + * @author Liyuan Li + * @version 0.1.0.0, Jan 13, 2019 + */ + +self.addEventListener('fetch', function (event) { + // fix 'cannot be installed: does not work offline' +}) \ No newline at end of file diff --git a/src/test/java/org/b3log/solo/AbstractTestCase.java b/src/test/java/org/b3log/solo/AbstractTestCase.java new file mode 100644 index 00000000..d5127aa3 --- /dev/null +++ b/src/test/java/org/b3log/solo/AbstractTestCase.java @@ -0,0 +1,481 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo; + +import org.apache.commons.lang.RandomStringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.ioc.BeanManager; +import org.b3log.latke.ioc.Discoverer; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.jdbc.util.Connections; +import org.b3log.latke.repository.jdbc.util.JdbcRepositories; +import org.b3log.latke.service.ServiceException; +import org.b3log.latke.util.Crypts; +import org.b3log.solo.cache.*; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.UserExt; +import org.b3log.solo.processor.MockDispatcherServlet; +import org.b3log.solo.repository.*; +import org.b3log.solo.service.*; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.util.Collection; +import java.util.Locale; + +/** + * Abstract test case. + * + * @author Liang Ding + * @version 3.0.0.2, Mar 29, 2019 + * @since 2.9.7 + */ +public abstract class AbstractTestCase { + + /** + * Bean manager. + */ + private BeanManager beanManager; + + /** + * Before class. + *
      + *
    1. Initializes Latke runtime
    2. + *
    3. Instantiates repositories
    4. + *
    + * + * @throws Exception exception + */ + @BeforeClass + public void beforeClass() throws Exception { + Latkes.init(); + Latkes.setLocale(Locale.SIMPLIFIED_CHINESE); + + final Collection> classes = Discoverer.discover("org.b3log.solo"); + BeanManager.start(classes); + beanManager = BeanManager.getInstance(); + + final Connection connection = Connections.getConnection(); + connection.createStatement().execute("DROP ALL OBJECTS"); + connection.close(); + + JdbcRepositories.initAllTables(); + } + + @BeforeMethod + public void beforeMethod(final Method method) { + System.out.println(method.getDeclaringClass().getSimpleName() + "#" + method.getName()); + } + + /** + * After class. + *
      + *
    • Clears all caches
    • + *
    + */ + @AfterClass + public void afterClass() { + final ArticleCache articleCache = beanManager.getReference(ArticleCache.class); + articleCache.clear(); + final CommentCache commentCache = beanManager.getReference(CommentCache.class); + commentCache.clear(); + final OptionCache optionCache = beanManager.getReference(OptionCache.class); + optionCache.clear(); + final PageCache pageCache = beanManager.getReference(PageCache.class); + pageCache.clear(); + final StatisticCache statisticCache = beanManager.getReference(StatisticCache.class); + statisticCache.clear(); + final UserCache userCache = beanManager.getReference(UserCache.class); + userCache.clear(); + } + + /** + * Init solo in test. + * + * @throws Exception exception + */ + public void init() throws Exception { + final InitService initService = getInitService(); + final JSONObject requestJSONObject = new JSONObject(); + requestJSONObject.put(User.USER_NAME, "Solo"); + requestJSONObject.put(UserExt.USER_B3_KEY, "pass"); + initService.init(requestJSONObject); + final UserQueryService userQueryService = getUserQueryService(); + Assert.assertNotNull(userQueryService.getUserByName("Solo")); + } + + /** + * Mocks admin login for console testing. + * + * @param request the specified request + * @throws ServiceException service exception + */ + public void mockAdminLogin(final MockHttpServletRequest request) throws ServiceException { + final JSONObject adminUser = getUserQueryService().getAdmin(); + final String userId = adminUser.optString(Keys.OBJECT_ID); + final JSONObject cookieJSONObject = new JSONObject(); + cookieJSONObject.put(Keys.OBJECT_ID, userId); + final String random = RandomStringUtils.randomAlphanumeric(16); + cookieJSONObject.put(Keys.TOKEN, "pass:" + random); + final String cookieValue = Crypts.encryptByAES(cookieJSONObject.toString(), Solos.COOKIE_SECRET); + final Cookie cookie = new Cookie(Solos.COOKIE_NAME, cookieValue); + request.setCookies(new Cookie[]{cookie}); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + } + + /** + * Gets a mock dispatcher servlet and run service. + * + * @param request the specified request + * @param response the specified response + * @return mock dispatcher servlet + */ + public MockDispatcherServlet mockDispatcherServletService(final HttpServletRequest request, final MockHttpServletResponse response) { + final MockDispatcherServlet ret = new MockDispatcherServlet(); + ret.init(); + SoloServletListener.routeConsoleProcessors(); + ret.service(request, response); + + return ret; + } + + /** + * Gets a mock request. + * + * @return mock request + */ + public MockHttpServletRequest mockRequest() { + final MockHttpServletRequest ret = new MockHttpServletRequest(); + + return ret; + } + + /** + * Gets a mock response. + * + * @return mock response + */ + public MockHttpServletResponse mockResponse() { + final StringWriter stringWriter = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(stringWriter); + final MockHttpServletResponse response = new MockHttpServletResponse(); + response.setWriter(printWriter); + response.setBodyWriter(stringWriter); + + return response; + } + + /** + * Gets category-tag repository. + * + * @return category-tag repository + */ + public CategoryTagRepository getCategoryTagRepository() { + return beanManager.getReference(CategoryTagRepository.class); + } + + /** + * Gets category repository. + * + * @return category repository + */ + public CategoryRepository getCategoryRepository() { + return beanManager.getReference(CategoryRepository.class); + } + + /** + * Gets user repository. + * + * @return user repository + */ + public UserRepository getUserRepository() { + return beanManager.getReference(UserRepository.class); + } + + /** + * Gets link repository. + * + * @return link repository + */ + public LinkRepository getLinkRepository() { + return beanManager.getReference(LinkRepository.class); + } + + /** + * Gets article repository. + * + * @return article repository + */ + public ArticleRepository getArticleRepository() { + return beanManager.getReference(ArticleRepository.class); + } + + /** + * Gets tag repository. + * + * @return tag repository + */ + public TagRepository getTagRepository() { + return beanManager.getReference(TagRepository.class); + } + + /** + * Gets tag-article repository. + * + * @return tag-article repository + */ + public TagArticleRepository getTagArticleRepository() { + return beanManager.getReference(TagArticleRepository.class); + } + + /** + * Gets page repository. + * + * @return page repository + */ + public PageRepository getPageRepository() { + return beanManager.getReference(PageRepository.class); + } + + /** + * Gets comment repository. + * + * @return comment repository + */ + public CommentRepository getCommentRepository() { + return beanManager.getReference(CommentRepository.class); + } + + /** + * Gets archive date repository. + * + * @return archive date repository + */ + public ArchiveDateRepository getArchiveDateRepository() { + return beanManager.getReference(ArchiveDateRepository.class); + } + + /** + * Archive date article repository. + * + * @return archive date article repository + */ + public ArchiveDateArticleRepository getArchiveDateArticleRepository() { + return beanManager.getReference(ArchiveDateArticleRepository.class); + } + + /** + * Gets plugin repository. + * + * @return plugin repository + */ + public PluginRepository getPluginRepository() { + return beanManager.getReference(PluginRepository.class); + } + + /** + * Gets option repository. + * + * @return option repository + */ + public OptionRepository getOptionRepository() { + return beanManager.getReference(OptionRepository.class); + } + + /** + * Gets category query service. + * + * @return category query service + */ + public CategoryQueryService getCategoryQueryService() { + return beanManager.getReference(CategoryQueryService.class); + } + + /** + * Gets category management service. + * + * @return category management service + */ + public CategoryMgmtService getCategoryMgmtService() { + return beanManager.getReference(CategoryMgmtService.class); + } + + /** + * Gets initialization service. + * + * @return initialization service + */ + public InitService getInitService() { + return beanManager.getReference(InitService.class); + } + + /** + * Gets user management service. + * + * @return user management service + */ + public UserMgmtService getUserMgmtService() { + return beanManager.getReference(UserMgmtService.class); + } + + /** + * Gets user query service. + * + * @return user query service + */ + public UserQueryService getUserQueryService() { + return beanManager.getReference(UserQueryService.class); + } + + /** + * Gets article management service. + * + * @return article management service + */ + public ArticleMgmtService getArticleMgmtService() { + return beanManager.getReference(ArticleMgmtService.class); + } + + /** + * Gets article query service. + * + * @return article query service + */ + public ArticleQueryService getArticleQueryService() { + return beanManager.getReference(ArticleQueryService.class); + } + + /** + * Gets page management service. + * + * @return page management service + */ + public PageMgmtService getPageMgmtService() { + return beanManager.getReference(PageMgmtService.class); + } + + /** + * Gets page query service. + * + * @return page query service + */ + public PageQueryService getPageQueryService() { + return beanManager.getReference(PageQueryService.class); + } + + /** + * Gets link management service. + * + * @return link management service + */ + public LinkMgmtService getLinkMgmtService() { + return beanManager.getReference(LinkMgmtService.class); + } + + /** + * Gets link query service. + * + * @return link query service + */ + public LinkQueryService getLinkQueryService() { + return beanManager.getReference(LinkQueryService.class); + } + + /** + * Gets preference management service. + * + * @return preference management service + */ + public PreferenceMgmtService getPreferenceMgmtService() { + return beanManager.getReference(PreferenceMgmtService.class); + } + + /** + * Gets tag query service. + * + * @return tag query service + */ + public TagQueryService getTagQueryService() { + return beanManager.getReference(TagQueryService.class); + } + + /** + * Gets tag management service. + * + * @return tag management service + */ + public TagMgmtService getTagMgmtService() { + return beanManager.getReference(TagMgmtService.class); + } + + /** + * Gets comment query service. + * + * @return comment query service + */ + public CommentQueryService getCommentQueryService() { + return beanManager.getReference(CommentQueryService.class); + } + + /** + * Gets comment management service. + * + * @return comment management service + */ + public CommentMgmtService getCommentMgmtService() { + return beanManager.getReference(CommentMgmtService.class); + } + + /** + * Gets archive date query service. + * + * @return archive date query service + */ + public ArchiveDateQueryService getArchiveDateQueryService() { + return beanManager.getReference(ArchiveDateQueryService.class); + } + + /** + * Gets option management service. + * + * @return option management service + */ + public OptionMgmtService getOptionMgmtService() { + return beanManager.getReference(OptionMgmtService.class); + } + + /** + * Gets option query service. + * + * @return option query service + */ + public OptionQueryService getOptionQueryService() { + return beanManager.getReference(OptionQueryService.class); + } +} diff --git a/src/test/java/org/b3log/solo/MockHttpServletRequest.java b/src/test/java/org/b3log/solo/MockHttpServletRequest.java new file mode 100644 index 00000000..629541d1 --- /dev/null +++ b/src/test/java/org/b3log/solo/MockHttpServletRequest.java @@ -0,0 +1,462 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo; + +import org.b3log.latke.Latkes; +import org.b3log.latke.servlet.HttpMethod; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.BufferedReader; +import java.security.Principal; +import java.util.*; + +/** + * Mock HTTP servlet request. + * + * @author Liang Ding + * @version 1.0.0.3, Mar 1, 2019 + */ +public class MockHttpServletRequest implements HttpServletRequest { + + /** + * Header. + */ + private Map headers = new HashMap<>(); + + /** + * Request URI. + */ + private String requestURI = "/"; + + /** + * Context path. + */ + private String contextPath = ""; + + /** + * Attributes. + */ + private Map attributes = new HashMap<>(); + + @Override + public String getAuthType() { + throw new UnsupportedOperationException("Not supported yet."); + } + + + private Cookie[] cookies; + + public void setCookies(final Cookie[] cookies) { + this.cookies = cookies; + } + + @Override + public Cookie[] getCookies() { + return cookies; + } + + @Override + public long getDateHeader(final String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + /** + * Sets header with the specified name and value. + * + * @param name the specified name + * @param value the specified value + */ + public void setHeader(final String name, final String value) { + headers.put(name, value); + } + + @Override + public String getHeader(final String name) { + return headers.get(name); + } + + @Override + public Enumeration getHeaders(final String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Enumeration getHeaderNames() { + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return false; + } + + @Override + public Object nextElement() { + return null; + } + }; + } + + @Override + public int getIntHeader(final String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + + private String method = HttpMethod.GET.toString(); + + public void setMethod(final String method) { + this.method = method; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String getPathInfo() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getPathTranslated() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getContextPath() { + return contextPath; + } + + @Override + public String getQueryString() { + return ""; + } + + @Override + public String getRemoteUser() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isUserInRole(final String role) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Principal getUserPrincipal() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getRequestedSessionId() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getRequestURI() { + return requestURI; + } + + /** + * Sets request URI with the specified request URI. + * + * @param requestURI the specified request URI + */ + public void setRequestURI(final String requestURI) { + this.requestURI = requestURI; + } + + @Override + public StringBuffer getRequestURL() { + return new StringBuffer(Latkes.getServePath() + requestURI); + } + + @Override + public String getServletPath() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public HttpSession getSession(final boolean create) { + return null; + } + + @Override + public HttpSession getSession() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isRequestedSessionIdValid() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isRequestedSessionIdFromURL() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isRequestedSessionIdFromUrl() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Object getAttribute(final String name) { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getCharacterEncoding() { + return "mock character encoding"; + } + + @Override + public void setCharacterEncoding(final String env) { + } + + @Override + public int getContentLength() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getContentType() { + return "mock content type"; + } + + @Override + public ServletInputStream getInputStream() { + throw new UnsupportedOperationException("Not supported yet."); + } + + private Map param = new HashMap<>(); + + public void putParameter(final String name, final String value) { + param.put(name, value); + } + + @Override + public String getParameter(final String name) { + return param.get(name); + } + + @Override + public Enumeration getParameterNames() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String[] getParameterValues(final String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Map getParameterMap() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getProtocol() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getScheme() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getServerName() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getServerPort() { + throw new UnsupportedOperationException("Not supported yet."); + } + + private BufferedReader reader; + + public void setReader(BufferedReader reader) { + this.reader = reader; + } + + @Override + public BufferedReader getReader() { + return reader; + } + + + private String remoteAddr; + + public void setRemoteAddr(final String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + @Override + public String getRemoteAddr() { + return remoteAddr; + } + + @Override + public String getRemoteHost() { + return "mock remote host"; + } + + @Override + public void setAttribute(final String name, final Object o) { + attributes.put(name, o); + } + + @Override + public void removeAttribute(final String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Locale getLocale() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Enumeration getLocales() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isSecure() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public RequestDispatcher getRequestDispatcher(final String path) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getRealPath(final String path) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalName() { + return "mock local name"; + } + + @Override + public String getLocalAddr() { + return "mock local addr"; + } + + @Override + public int getLocalPort() { + return 0; + } + + @Override + public String changeSessionId() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean authenticate(final HttpServletResponse response) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void login(final String username, final String password) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void logout() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Collection getParts() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Part getPart(final String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public T upgrade(final Class handlerClass) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long getContentLengthLong() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public ServletContext getServletContext() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public AsyncContext startAsync(final ServletRequest servletRequest, final ServletResponse servletResponse) throws IllegalStateException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isAsyncStarted() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isAsyncSupported() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public AsyncContext getAsyncContext() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public DispatcherType getDispatcherType() { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/src/test/java/org/b3log/solo/MockHttpServletResponse.java b/src/test/java/org/b3log/solo/MockHttpServletResponse.java new file mode 100644 index 00000000..95c219a7 --- /dev/null +++ b/src/test/java/org/b3log/solo/MockHttpServletResponse.java @@ -0,0 +1,265 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.*; + +/** + * Mock HTTP servlet request. + * + * @author Liang Ding + * @version 1.0.0.2, May 1, 2012 + * @since 2.4.34 + */ +public class MockHttpServletResponse implements HttpServletResponse { + + private List cookies = new ArrayList<>(); + + @Override + public void addCookie(Cookie cookie) { + cookies.add(cookie); + } + + public List cookies() { + return cookies; + } + + @Override + public boolean containsHeader(String name) { + return false; + } + + @Override + public String encodeURL(String url) { + return null; + } + + @Override + public String encodeRedirectURL(String url) { + return null; + } + + @Override + public String encodeUrl(String url) { + return null; + } + + @Override + public String encodeRedirectUrl(String url) { + return null; + } + + @Override + public void sendError(int sc, String msg) { + + } + + @Override + public void sendError(int sc) { + + } + + @Override + public void sendRedirect(String location) { + + } + + @Override + public void setDateHeader(String name, long date) { + + } + + @Override + public void addDateHeader(String name, long date) { + + } + + private Map headers = new HashMap<>(); + + @Override + public void setHeader(String name, String value) { + headers.put(name, value); + } + + @Override + public String getHeader(String name) { + return headers.get(name); + } + + @Override + public void addHeader(String name, String value) { + headers.put(name, value); + } + + @Override + public void setIntHeader(String name, int value) { + + } + + @Override + public void addIntHeader(String name, int value) { + + } + + @Override + public void setStatus(int sc) { + + } + + @Override + public void setStatus(int sc, String sm) { + + } + + @Override + public int getStatus() { + return 0; + } + + @Override + public Collection getHeaders(String name) { + return null; + } + + @Override + public Collection getHeaderNames() { + return null; + } + + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public String getContentType() { + return null; + } + + private long outputBytes; + + public long outputBytes() { + return outputBytes; + } + + @Override + public ServletOutputStream getOutputStream() { + return new ServletOutputStream() { + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + + } + + @Override + public void write(int b) { + outputBytes++; + } + }; + } + + private StringWriter bodyWriter; + private PrintWriter writer; + + public void setWriter(final PrintWriter writer) { + this.writer = writer; + } + + public void setBodyWriter(final StringWriter bodyWriter) { + this.bodyWriter = bodyWriter; + } + + public String body() { + return bodyWriter.toString(); + } + + @Override + public PrintWriter getWriter() { + return writer; + } + + @Override + public void setCharacterEncoding(String charset) { + + } + + @Override + public void setContentLength(int len) { + + } + + @Override + public void setContentLengthLong(long len) { + + } + + @Override + public void setContentType(String type) { + + } + + @Override + public void setBufferSize(int size) { + + } + + @Override + public int getBufferSize() { + return 0; + } + + @Override + public void flushBuffer() { + + } + + @Override + public void resetBuffer() { + + } + + @Override + public boolean isCommitted() { + return false; + } + + @Override + public void reset() { + + } + + @Override + public void setLocale(Locale loc) { + + } + + @Override + public Locale getLocale() { + return null; + } +} diff --git a/src/test/java/org/b3log/solo/processor/ArticleProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/ArticleProcessorTestCase.java new file mode 100644 index 00000000..721bbe64 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/ArticleProcessorTestCase.java @@ -0,0 +1,260 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DateFormatUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Query; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link ArticleProcessor} test case. + * + * @author Liang Ding + * @version 1.0.1.4, Feb 22, 2019 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class ArticleProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getArchivesArticlesByPage. + */ + @Test(dependsOnMethods = "init") + public void getArchivesArticlesByPage() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/articles/archives/" + DateFormatUtils.format(System.currentTimeMillis(), "yyyy/MM")); + request.putParameter("p", "1"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "{\"sc\":true")); + } + + /** + * getArticleContent. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticleContent() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/get-article-content"); + request.putParameter("id", articleId); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Solo 博客系统已经初始化完毕")); + } + + /** + * getArticlesByPage. + */ + @Test(dependsOnMethods = "init") + public void getArticlesByPage() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/articles"); + request.putParameter("p", "1"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "{\"sc\":true")); + } + + /** + * getAuthorsArticlesByPage. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getAuthorsArticlesByPage() throws Exception { + final JSONObject admin = getUserRepository().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/articles/authors/" + userId); + request.putParameter("p", "1"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "{\"sc\":true")); + } + + /** + * getRandomArticles. + */ + @Test(dependsOnMethods = "init") + public void getRandomArticles() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/articles/random"); + request.setMethod("POST"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "{\"randomArticles")); + } + + /** + * getRelevantArticles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getRelevantArticles() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/article/id/" + articleId + "/relevant/articles"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "{\"relevantArticles\"")); + } + + /** + * getTagArticlesByPage. + */ + @Test(dependsOnMethods = "init") + public void getTagArticlesByPage() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/articles/tags/Solo"); + request.putParameter("p", "1"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "{\"sc\":true")); + } + + /** + * showArchiveArticles. + */ + @Test(dependsOnMethods = "init") + public void showArchiveArticles() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/archives/" + DateFormatUtils.format(System.currentTimeMillis(), "yyyy/MM")); + request.putParameter("p", "1"); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Solo 的个人博客")); + } + + /** + * showArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void showArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/article"); + request.setAttribute(Article.ARTICLE, article); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Solo 的个人博客")); + } + + /** + * showArticlePwdForm. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void showArticlePwdForm() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article-pwd"); + request.putParameter("articleId", articleId); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "访问密码 - Solo 的个人博客")); + } + + /** + * showAuthorArticles. + * + * @throws Exception exception + */ +// @Test(dependsOnMethods = "init") +// public void showAuthorArticles() throws Exception { +// final JSONObject admin = getUserRepository().getAdmin(); +// final String userId = admin.optString(Keys.OBJECT_ID); +// +// final HttpServletRequest request = mock(HttpServletRequest.class); +// when(request.getServletContext()).thenReturn(mock(ServletContext.class)); +// when(request.getRequestURI()).thenReturn("/authors/" + userId + "/1"); +// when(request.getMethod()).thenReturn("GET"); +// when(request.getAttribute(Keys.TEMAPLTE_DIR_NAME)).thenReturn(Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); +// when(request.getAttribute(Keys.HttpRequest.START_TIME_MILLIS)).thenReturn(System.currentTimeMillis()); +// +// final MockDispatcherServlet dispatcherServlet = new MockDispatcherServlet(); +// dispatcherServlet.init(); +// +// final StringWriter stringWriter = new StringWriter(); +// final PrintWriter printWriter = new PrintWriter(stringWriter); +// +// final HttpServletResponse response = mock(HttpServletResponse.class); +// when(response.getWriter()).thenReturn(printWriter); +// +// dispatcherServlet.service(request, response); +// +// final String content = stringWriter.toString(); +// Assert.assertTrue(StringUtils.contains(content, "Solo 示例") || StringUtils.isBlank(content)); +// } +} diff --git a/src/test/java/org/b3log/solo/processor/BlogProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/BlogProcessorTestCase.java new file mode 100644 index 00000000..1b2a75b5 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/BlogProcessorTestCase.java @@ -0,0 +1,75 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link BlogProcessor} test case. + * + * @author Liang Ding + * @version 1.0.0.2, Oct 28, 2018 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class BlogProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getBlogInfo. + */ + @Test(dependsOnMethods = "init") + public void getBlogInfo() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/blog/info"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.startsWith(content, "{\"staticServePath\":\"http://localhost:8080\"")); + } + + /** + * getArticlesTags. + */ + @Test(dependsOnMethods = "init") + public void getArticlesTags() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/blog/articles-tags"); + request.putParameter("pwd", "pass"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.startsWith(content, "{\"data\":")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/CategoryProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/CategoryProcessorTestCase.java new file mode 100644 index 00000000..e1660f51 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/CategoryProcessorTestCase.java @@ -0,0 +1,87 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link CategoryProcessor} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class CategoryProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * showCategoryArticles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void showCategoryArticles() throws Exception { + MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/category/"); + request.setMethod("POST"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Category.CATEGORY_T_TAGS, "Solo"); + requestJSON.put(Category.CATEGORY_TITLE, "分类1"); + requestJSON.put(Category.CATEGORY_URI, "cate1"); + + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + request = mockRequest(); + request.setRequestURI("/category/cate1"); + request.setMethod("GET"); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "分类1 - Solo 的个人博客")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/CommentProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/CommentProcessorTestCase.java new file mode 100644 index 00000000..6384e00d --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/CommentProcessorTestCase.java @@ -0,0 +1,129 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.service.ServiceException; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Option; +import org.b3log.solo.model.Page; +import org.b3log.solo.service.ArticleMgmtService; +import org.b3log.solo.service.PageMgmtService; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link CommentProcessorTestCase} test case. + * + * @author Liang Ding + * @version 1.0.0.6, Apr 19, 2019 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class CommentProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * addArticleComment. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addArticleComment() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/article/comments"); + request.setMethod("POST"); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + + final JSONObject requestJSON = new JSONObject(); + requestJSON.put("oId", addArticle()); + requestJSON.put("commentName", "88250"); + requestJSON.put("commentEmail", "d@hacpai.com"); + requestJSON.put("commentURL", "https://hacpai.com"); + requestJSON.put("commentContent", "测试评论"); + + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + private String addPage() throws ServiceException { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p1"); + page.put(Page.PAGE_TITLE, "page1 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + return pageMgmtService.addPage(requestJSONObject); + } + + private String addArticle() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject article = new JSONObject(); + requestJSONObject.put(Article.ARTICLE, article); + + final JSONObject admin = getUserQueryService().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + article.put(Article.ARTICLE_AUTHOR_ID, userId); + article.put(Article.ARTICLE_TITLE, "article1 title"); + article.put(Article.ARTICLE_ABSTRACT, "article1 abstract"); + article.put(Article.ARTICLE_CONTENT, "article1 content"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2, tag3"); + article.put(Article.ARTICLE_PERMALINK, "article1 permalink"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, true); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + + return articleMgmtService.addArticle(requestJSONObject); + } +} diff --git a/src/test/java/org/b3log/solo/processor/ErrorProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/ErrorProcessorTestCase.java new file mode 100644 index 00000000..e0c88db7 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/ErrorProcessorTestCase.java @@ -0,0 +1,60 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link ErrorProcessor} test case. + * + * @author Liang Ding + * @version 1.0.1.3, Feb 22, 2019 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class ErrorProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * showErrorPage. + */ + @Test(dependsOnMethods = "init") + public void showErrorPage() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/error/403"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "403 Forbidden! - Solo 的个人博客")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/FeedProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/FeedProcessorTestCase.java new file mode 100644 index 00000000..c30525af --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/FeedProcessorTestCase.java @@ -0,0 +1,75 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link FeedProcessor} test case. + * + * @author Liang Ding + * @version 1.0.0.3, Sep 26, 2018 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class FeedProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * blogArticlesAtom. + * + */ + @Test(dependsOnMethods = "init") + public void blogArticlesAtom() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/atom.xml"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.startsWith(content, ". + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.util.Solos; +import org.testng.Assert; +import org.testng.annotations.Test; + +import javax.servlet.http.Cookie; +import java.util.List; + +/** + * {@link IndexProcessor} test case. + * + * @author Liang Ding + * @version 1.0.1.4, Feb 22, 2019 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class IndexProcessorTestCase extends AbstractTestCase { + + /** + * showStart. + */ + @Test + public void showStart() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/start"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "欢迎使用! - Solo")); + } + + /** + * Init. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "showStart") + public void init() throws Exception { + super.init(); + } + + /** + * showIndex. + */ + @Test(dependsOnMethods = "init") + public void showIndex() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Solo 的个人博客")); + } + + /** + * showKillBrowser. + */ + @Test(dependsOnMethods = "init") + public void showKillBrowser() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/kill-browser"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Kill IE! - Solo 的个人博客")); + } + + /** + * logout. + */ + @Test(dependsOnMethods = "init") + public void logout() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/logout"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final List cookies = response.cookies(); + Assert.assertEquals(cookies.size(), 1); + Assert.assertEquals(cookies.get(0).getName(), Solos.COOKIE_NAME); + Assert.assertNull(cookies.get(0).getValue()); + } +} diff --git a/src/test/java/org/b3log/solo/processor/MockDispatcherServlet.java b/src/test/java/org/b3log/solo/processor/MockDispatcherServlet.java new file mode 100644 index 00000000..9fedb2ba --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/MockDispatcherServlet.java @@ -0,0 +1,52 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.b3log.latke.servlet.DispatcherServlet; +import org.b3log.latke.servlet.handler.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** + * Mock dispatcher servlet for unit tests. + * + * @author Liang Ding + * @version 1.0.0.2, Dec 5, 2018 + * @since 1.7.0 + */ +public class MockDispatcherServlet { + + /** + * Handlers + */ + private static final List HANDLERS = new ArrayList<>(); + + public void init() { + HANDLERS.add(new RouteHandler()); + HANDLERS.add(new BeforeHandleHandler()); + HANDLERS.add(new ContextHandleHandler()); + HANDLERS.add(new AfterHandleHandler()); + } + + public void service(final HttpServletRequest req, final HttpServletResponse resp) { + DispatcherServlet.handle(req, resp); + } +} diff --git a/src/test/java/org/b3log/solo/processor/SearchProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/SearchProcessorTestCase.java new file mode 100644 index 00000000..d56e44c3 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/SearchProcessorTestCase.java @@ -0,0 +1,76 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link SearchProcessor} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class SearchProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * showOpensearchXML. + */ + @Test(dependsOnMethods = "init") + public void showOpensearchXML() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/opensearch.xml"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "")); + } + + /** + * search. + */ + @Test(dependsOnMethods = "init") + public void search() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/search"); + + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Solo 的个人博客搜索")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/SitemapProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/SitemapProcessorTestCase.java new file mode 100644 index 00000000..a918c4ba --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/SitemapProcessorTestCase.java @@ -0,0 +1,60 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link SitemapProcessor} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Nov 5, 2016 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class SitemapProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * sitemap. + */ + @Test(dependsOnMethods = "init") + public void sitemap() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/sitemap.xml"); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.startsWith(content, ". + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Option; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link TagProcessor} test case. + * + * @author Liang Ding + * @version 1.0.1.1, May 29, 2018 + * @since 1.7.0 + */ +@Test(suiteName = "processor") +public class TagProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * showTagArticles. + */ + @Test(dependsOnMethods = "init") + public void showTagArticles() { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/tags/Solo"); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Solo 标签 - Solo 的个人博客")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/UserTemplateProcessorTestCase.java b/src/test/java/org/b3log/solo/processor/UserTemplateProcessorTestCase.java new file mode 100644 index 00000000..eea9b385 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/UserTemplateProcessorTestCase.java @@ -0,0 +1,63 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Option; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link UserTemplateProcessor} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class UserTemplateProcessorTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * showCategoryArticles. + */ + @Test(dependsOnMethods = "init") + public void showCategoryArticles() { + MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/links.html"); + request.setAttribute(Keys.TEMAPLTE_DIR_NAME, Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME); + MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "友情链接 - Solo 的个人博客")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/AdminConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/AdminConsoleTestCase.java new file mode 100644 index 00000000..c0fcfd80 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/AdminConsoleTestCase.java @@ -0,0 +1,154 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link AdminConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.2, Feb 22, 2019 + * @since 2.9.7 + */ +@Test(suiteName = "processor") +public class AdminConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * showAdminIndex. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void showAdminIndex() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/admin-index.do"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "后台管理 - Solo 的个人博客")); + } + + /** + * showAdminFunctions. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void showAdminFunctions() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/admin-article.do"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "
    ")); + } + + /** + * showAdminPreferenceFunction. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void showAdminPreferenceFunction() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/admin-preference.do"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "信息配置")); + } + + /** + * exportSQL. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void exportSQL() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/export/sql"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final long outputBytes = response.outputBytes(); + Assert.assertTrue(0 < outputBytes); + } + + /** + * exportJSON. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void exportJSON() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/export/json"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final long outputBytes = response.outputBytes(); + Assert.assertTrue(0 < outputBytes); + } + + /** + * exportHexo. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void exportHexo() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/export/hexo"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final long outputBytes = response.outputBytes(); + Assert.assertTrue(0 < outputBytes); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/ArticleConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/ArticleConsoleTestCase.java new file mode 100644 index 00000000..f2e38c0a --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/ArticleConsoleTestCase.java @@ -0,0 +1,276 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Query; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Article; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link ArticleConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Feb 6, 2019 + * @since 2.9.7 + */ +@Test(suiteName = "processor") +public class ArticleConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getArticleThumbs. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticleThumbs() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/thumbs"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * markdown2HTML. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void markdown2HTML() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/markdown/2html"); + request.setMethod("POST"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put("markdownText", "**Solo**"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "

    Solo<\\/strong><\\/p>")); + } + + /** + * getArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/" + articleId); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * getArticles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticles() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/articles/status/published/1/10/20"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * removeArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "updateArticle") + public void removeArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/" + articleId); + request.setMethod("DELETE"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * cancelPublishArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void cancelPublishArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/unpublish/" + articleId); + request.setMethod("PUT"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * cancelTopArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void cancelTopArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/canceltop/" + articleId); + request.setMethod("PUT"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * putTopArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void putTopArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/puttop/" + articleId); + request.setMethod("PUT"); + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * updateArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updateArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Article.ARTICLE, article); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } + + /** + * addArticle. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addArticle() throws Exception { + final JSONObject article = getArticleRepository().get(new Query()).optJSONArray(Keys.RESULTS).optJSONObject(0); + article.put(Keys.OBJECT_ID, ""); + article.put(Article.ARTICLE_PERMALINK, ""); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/"); + request.setMethod("POST"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Article.ARTICLE, article); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "\"sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/CategoryConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/CategoryConsoleTestCase.java new file mode 100644 index 00000000..2584227f --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/CategoryConsoleTestCase.java @@ -0,0 +1,204 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Category; +import org.b3log.solo.model.Common; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link CategoryConsole} test case. + * + * @author Liang Ding + * @version 1.1.0.0, Dec 10, 2018 + * @since 2.1.0 + */ +@Test(suiteName = "processor") +public class CategoryConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * addCategory. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addCategory() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/category/"); + request.setMethod("POST"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Category.CATEGORY_T_TAGS, "Solo"); + requestJSON.put(Category.CATEGORY_TITLE, "分类1"); + requestJSON.put(Category.CATEGORY_URI, "cate1"); + + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getCategory. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addCategory") + public void getCategory() throws Exception { + final JSONObject category = getCategoryQueryService().getByTitle("分类1"); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/category/" + category.optString(Keys.OBJECT_ID)); + request.setMethod("GET"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * updateCategory. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addCategory") + public void updateCategory() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/category/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Category.CATEGORY_T_TAGS, "Solo"); + JSONObject category = getCategoryQueryService().getByTitle("分类1"); + requestJSON.put(Keys.OBJECT_ID, category.optString(Keys.OBJECT_ID)); + requestJSON.put(Category.CATEGORY_TITLE, "新的分类1"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + + category = getCategoryQueryService().getByTitle("分类1"); + Assert.assertNull(category); + + category = getCategoryQueryService().getByTitle("新的分类1"); + Assert.assertNotNull(category); + Assert.assertEquals(category.optInt(Category.CATEGORY_TAG_CNT), 1); // https://github.com/b3log/solo/issues/12274 + } + + /** + * getCategories. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "updateCategory") + public void getCategories() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/categories/1/10/20"); + request.setMethod("GET"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * changeOrder. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getCategories") + public void changeOrder() throws Exception { + final JSONObject category = getCategoryQueryService().getByTitle("新的分类1"); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/category/order/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Keys.OBJECT_ID, category.optString(Keys.OBJECT_ID)); + requestJSON.put(Common.DIRECTION, "up"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * removeCategory. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "changeOrder") + public void removeCategory() throws Exception { + final JSONObject category = getCategoryQueryService().getByTitle("新的分类1"); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/category/" + category.optString(Keys.OBJECT_ID)); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/CommentConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/CommentConsoleTestCase.java new file mode 100644 index 00000000..73426110 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/CommentConsoleTestCase.java @@ -0,0 +1,116 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link CommentConsole} test case. + * + * @author Liang Ding + * @version 1.1.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class CommentConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getComments. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getComments() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/comments/1/10/20"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getArticleComments. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticleComments() throws Exception { + final List recentArticles = getArticleQueryService().getRecentArticles(1); + final JSONObject article = recentArticles.get(0); + final String articleId = article.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/comments/article/" + articleId); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * removeArticleComment. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removeArticleComment() throws Exception { + final List recentComments = getCommentRepository().getRecentComments(1); + final JSONObject comment = recentComments.get(0); + final String commentId = comment.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/article/comment/" + commentId); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/LinkConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/LinkConsoleTestCase.java new file mode 100644 index 00000000..2426b5f5 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/LinkConsoleTestCase.java @@ -0,0 +1,206 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Query; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Link; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link LinkConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class LinkConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * addLink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addLink() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/link/"); + request.setMethod("POST"); + final JSONObject requestJSON = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSON.put(Link.LINK, link); + link.put(Link.LINK_TITLE, "黑客派"); + link.put(Link.LINK_ADDRESS, "https://hacpai.com"); + link.put(Link.LINK_DESCRIPTION, "黑客与画家的社区"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * updateLink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addLink") + public void updateLink() throws Exception { + final JSONObject l = getLinkRepository().getList(new Query()).get(0); + final String linkId = l.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/link/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSON.put(Link.LINK, link); + link.put(Keys.OBJECT_ID, linkId); + link.put(Link.LINK_TITLE, "黑客派"); + link.put(Link.LINK_ADDRESS, "https://hacpai.com"); + link.put(Link.LINK_DESCRIPTION, "B3log 开源社区线上论坛"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * changeOrder. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "updateLink") + public void changeOrder() throws Exception { + final JSONObject l = getLinkRepository().getList(new Query()).get(0); + final String linkId = l.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/link/order/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Keys.OBJECT_ID, linkId); + requestJSON.put(Common.DIRECTION, "up"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getLink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "changeOrder") + public void getLink() throws Exception { + final JSONObject l = getLinkRepository().getList(new Query()).get(0); + final String linkId = l.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/link/" + linkId); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getLinks. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getLink") + public void getLinks() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/links/1/10/20"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * removeLink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getLinks") + public void removeLink() throws Exception { + final JSONObject l = getLinkRepository().getList(new Query()).get(0); + final String linkId = l.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/link/" + linkId); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/OtherConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/OtherConsoleTestCase.java new file mode 100644 index 00000000..6abcd194 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/OtherConsoleTestCase.java @@ -0,0 +1,66 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link OtherConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Mar 23, 2019 + * @since 3.4.0 + */ +@Test(suiteName = "processor") +public class OtherConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * removeUnusedArchives. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removeUnusedArchives() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/archive/unused"); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/PageConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/PageConsoleTestCase.java new file mode 100644 index 00000000..7c2b4ede --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/PageConsoleTestCase.java @@ -0,0 +1,201 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Query; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Common; +import org.b3log.solo.model.Page; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link PageConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Apr 19, 2019 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class PageConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * addPage. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addPage() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/page/"); + request.setMethod("POST"); + final JSONObject requestJSON = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSON.put(Page.PAGE, page); + page.put(Page.PAGE_TITLE, "黑客派"); + page.put(Page.PAGE_PERMALINK, "https://hacpai.com"); + page.put(Page.PAGE_OPEN_TARGET, ""); + page.put(Page.PAGE_ICON, ""); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * updatePage. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addPage") + public void updatePage() throws Exception { + final JSONObject p = getPageRepository().getList(new Query()).get(0); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/page/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Page.PAGE, p); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * changeOrder. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "updatePage") + public void changeOrder() throws Exception { + final JSONObject p = getPageRepository().getList(new Query()).get(0); + final String pageId = p.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/page/order/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Keys.OBJECT_ID, pageId); + requestJSON.put(Common.DIRECTION, "up"); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getPage. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "changeOrder") + public void getPage() throws Exception { + final JSONObject p = getPageRepository().getList(new Query()).get(0); + final String pageId = p.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/page/" + pageId); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getPages. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getPage") + public void getPages() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/pages/1/10/20"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * removePage. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getPages") + public void removeLink() throws Exception { + final JSONObject p = getPageRepository().getList(new Query()).get(0); + final String pageId = p.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/page/" + pageId); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/PluginConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/PluginConsoleTestCase.java new file mode 100644 index 00000000..84a161e9 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/PluginConsoleTestCase.java @@ -0,0 +1,65 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link PluginConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class PluginConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getPlugins. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getPlugins() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/plugins/1/10/20"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/PreferenceConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/PreferenceConsoleTestCase.java new file mode 100644 index 00000000..b3385ea3 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/PreferenceConsoleTestCase.java @@ -0,0 +1,115 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link PreferenceConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Dec 23, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class PreferenceConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getSigns. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getSigns() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/signs/"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getPreference. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getPreference() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/preference/"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * updatePreference. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updatePreference() throws Exception { + final JSONObject p = getOptionQueryService().getPreference(); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/preference/"); + request.setMethod("PUT"); + final JSONObject requestJSON = new JSONObject(); + requestJSON.put(Option.CATEGORY_C_PREFERENCE, p); + final BufferedReader reader = new BufferedReader(new StringReader(requestJSON.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/RepairConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/RepairConsoleTestCase.java new file mode 100644 index 00000000..d43ed22f --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/RepairConsoleTestCase.java @@ -0,0 +1,65 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link RepairConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class RepairConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * restoreSigns. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void restoreSigns() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/fix/restore-signs"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "Restore signs succeeded.")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/TagConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/TagConsoleTestCase.java new file mode 100644 index 00000000..0438606d --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/TagConsoleTestCase.java @@ -0,0 +1,104 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link TagConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 11, 2018 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class TagConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * getTags. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getTags() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/tags"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getUnusedTags. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getTags") + public void getUnusedTags() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/tag/unused"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * removeUnusedTags. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getUnusedTags") + public void removeUnusedTags() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/tag/unused"); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/processor/console/UserConsoleTestCase.java b/src/test/java/org/b3log/solo/processor/console/UserConsoleTestCase.java new file mode 100644 index 00000000..a9b76405 --- /dev/null +++ b/src/test/java/org/b3log/solo/processor/console/UserConsoleTestCase.java @@ -0,0 +1,163 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.processor.console; + +import org.apache.commons.lang.StringUtils; +import org.b3log.latke.Keys; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.Query; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.MockHttpServletRequest; +import org.b3log.solo.MockHttpServletResponse; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +/** + * {@link UserConsole} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Feb 8, 2019 + * @since 2.9.8 + */ +@Test(suiteName = "processor") +public class UserConsoleTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * updateUser. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updateUser() throws Exception { + final JSONObject u = getUserRepository().getFirst(new Query()); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/user/"); + request.setMethod("PUT"); + final BufferedReader reader = new BufferedReader(new StringReader(u.toString())); + request.setReader(reader); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getUser. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "updateUser") + public void getUser() throws Exception { + final JSONObject u = getUserRepository().getFirst(new Query()); + final String userId = u.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/user/" + userId); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * getUsers. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getUser") + public void getUsers() throws Exception { + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/users/1/10/20"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * changeUserRole. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getUsers") + public void changeUserRole() throws Exception { + final JSONObject u = getUserRepository().getFirst(new Query()); + final String userId = u.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/changeRole/" + userId); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } + + /** + * removeUser. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "changeUserRole") + public void removeUser() throws Exception { + final JSONObject u = getUserRepository().getFirst(new Query()); + final String userId = u.optString(Keys.OBJECT_ID); + + final MockHttpServletRequest request = mockRequest(); + request.setRequestURI("/console/user/" + userId); + request.setMethod("DELETE"); + + mockAdminLogin(request); + + final MockHttpServletResponse response = mockResponse(); + mockDispatcherServletService(request, response); + + final String content = response.body(); + Assert.assertTrue(StringUtils.contains(content, "sc\":true")); + } +} diff --git a/src/test/java/org/b3log/solo/repository/ArchiveDateArticleRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/ArchiveDateArticleRepositoryImplTestCase.java new file mode 100644 index 00000000..022a5bc3 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/ArchiveDateArticleRepositoryImplTestCase.java @@ -0,0 +1,91 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.ArchiveDate; +import org.b3log.solo.model.Article; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link ArchiveDateArticleRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 31, 2011 + */ +@Test(suiteName = "repository") +public class ArchiveDateArticleRepositoryImplTestCase extends AbstractTestCase { + + /** + * Adds successfully. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final ArchiveDateArticleRepository archiveDateArticleRepository = getArchiveDateArticleRepository(); + + final JSONObject archiveDateArticle = new JSONObject(); + + archiveDateArticle.put(ArchiveDate.ARCHIVE_DATE + "_" + Keys.OBJECT_ID, "archiveDateId"); + archiveDateArticle.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, "articleId"); + + final Transaction transaction = archiveDateArticleRepository.beginTransaction(); + archiveDateArticleRepository.add(archiveDateArticle); + transaction.commit(); + + final JSONObject found = archiveDateArticleRepository.getByArticleId("articleId"); + Assert.assertNotNull(found); + + final JSONObject notFound = archiveDateArticleRepository.getByArticleId("not found"); + Assert.assertNull(notFound); + } + + /** + * Get By ArchiveDate Id. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByArchiveDateId() throws Exception { + final ArchiveDateArticleRepository archiveDateArticleRepository = getArchiveDateArticleRepository(); + + final JSONObject found = archiveDateArticleRepository.getByArchiveDateId("archiveDateId", 1, Integer.MAX_VALUE); + Assert.assertNotNull(found); + + final JSONObject notFound = archiveDateArticleRepository.getByArchiveDateId("not found", 1, Integer.MAX_VALUE); + Assert.assertNotNull(notFound); + } + + /** + * Get By Archive Id. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByArticleId() throws Exception { + final ArchiveDateArticleRepository archiveDateArticleRepository = getArchiveDateArticleRepository(); + + Assert.assertNotNull(archiveDateArticleRepository.getByArticleId("articleId")); + Assert.assertNull(archiveDateArticleRepository.getByArticleId("not found")); + } +} diff --git a/src/test/java/org/b3log/solo/repository/ArchiveDateRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/ArchiveDateRepositoryImplTestCase.java new file mode 100644 index 00000000..289252c3 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/ArchiveDateRepositoryImplTestCase.java @@ -0,0 +1,72 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import junit.framework.Assert; +import org.apache.commons.lang.time.DateUtils; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.ArchiveDate; +import org.json.JSONObject; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link ArchiveDateRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.3, Jan 28, 2019 + */ +@Test(suiteName = "repository") +public class ArchiveDateRepositoryImplTestCase extends AbstractTestCase { + + /** + * Adds successfully. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final ArchiveDateRepository archiveDateRepository = getArchiveDateRepository(); + + final JSONObject archiveDate = new JSONObject(); + archiveDate.put(ArchiveDate.ARCHIVE_TIME, DateUtils.parseDate("2011/12", new String[]{"yyyy/MM"}).getTime()); + + final Transaction transaction = archiveDateRepository.beginTransaction(); + archiveDateRepository.add(archiveDate); + transaction.commit(); + + final List archiveDates = archiveDateRepository.getArchiveDates(); + Assert.assertNotNull(archiveDates); + Assert.assertEquals(1, archiveDates.size()); + } + + /** + * Get By ArchiveDate. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByArchiveDate() throws Exception { + final ArchiveDateRepository archiveDateRepository = getArchiveDateRepository(); + + final JSONObject archiveDate = archiveDateRepository.getByArchiveDate("2011/12"); + Assert.assertNotNull(archiveDate); + } +} diff --git a/src/test/java/org/b3log/solo/repository/ArticleRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/ArticleRepositoryImplTestCase.java new file mode 100644 index 00000000..1ccd6a94 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/ArticleRepositoryImplTestCase.java @@ -0,0 +1,294 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Article; +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.List; + +/** + * {@link ArticleRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.5, Feb 25, 2019 + */ +@Test(suiteName = "repository") +public final class ArticleRepositoryImplTestCase extends AbstractTestCase { + + /** + * Adds successfully. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + final JSONObject article = new JSONObject(); + + article.put(Article.ARTICLE_TITLE, "article title1"); + article.put(Article.ARTICLE_ABSTRACT, "article abstract"); + article.put(Article.ARTICLE_ABSTRACT_TEXT, "article abstract text"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2"); + article.put(Article.ARTICLE_AUTHOR_ID, "1"); + article.put(Article.ARTICLE_COMMENT_COUNT, 0); + article.put(Article.ARTICLE_VIEW_COUNT, 0); + article.put(Article.ARTICLE_CONTENT, "article content"); + article.put(Article.ARTICLE_PERMALINK, "article permalink1"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Article.ARTICLE_PUT_TOP, false); + article.put(Article.ARTICLE_CREATED, new Date().getTime()); + article.put(Article.ARTICLE_UPDATED, new Date().getTime()); + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + article.put(Article.ARTICLE_IMG1_URL, Article.getArticleImg1URL(article)); + + final Transaction transaction = articleRepository.beginTransaction(); + articleRepository.add(article); + transaction.commit(); + + final JSONArray results = articleRepository.getByAuthorId("1", 1, Integer.MAX_VALUE).getJSONArray(Keys.RESULTS); + + Assert.assertEquals(results.length(), 1); + } + + /** + * Get by permalink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByPermalink() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + final JSONObject article = articleRepository.getByPermalink("article permalink1"); + + Assert.assertNotNull(article); + Assert.assertEquals(article.getString(Article.ARTICLE_TITLE), "article title1"); + + Assert.assertNull(articleRepository.getByPermalink("not found")); + } + + /** + * Get by permalink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add"}) + public void previousAndNext() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + final JSONObject article = new JSONObject(); + + article.put(Article.ARTICLE_TITLE, "article title2"); + article.put(Article.ARTICLE_ABSTRACT, "article abstract"); + article.put(Article.ARTICLE_ABSTRACT_TEXT, "article abstract text"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2"); + article.put(Article.ARTICLE_AUTHOR_ID, "1"); + article.put(Article.ARTICLE_COMMENT_COUNT, 1); + article.put(Article.ARTICLE_VIEW_COUNT, 1); + article.put(Article.ARTICLE_CONTENT, "article content"); + article.put(Article.ARTICLE_PERMALINK, "article permalink2"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Article.ARTICLE_PUT_TOP, false); + article.put(Article.ARTICLE_CREATED, new Date().getTime()); + article.put(Article.ARTICLE_UPDATED, new Date().getTime()); + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + article.put(Article.ARTICLE_IMG1_URL, Article.getArticleImg1URL(article)); + + final Transaction transaction = articleRepository.beginTransaction(); + articleRepository.add(article); + transaction.commit(); + + Assert.assertEquals(articleRepository.count(), 2); + + JSONObject previousArticle = articleRepository.getPreviousArticle(article.getString(Keys.OBJECT_ID)); + + Assert.assertNotNull(previousArticle); + Assert.assertEquals(previousArticle.getString(Article.ARTICLE_TITLE), "article title1"); + Assert.assertEquals(previousArticle.getString(Article.ARTICLE_PERMALINK), "article permalink1"); + Assert.assertNull(previousArticle.opt(Keys.OBJECT_ID)); + + previousArticle = articleRepository.getByPermalink(previousArticle.getString(Article.ARTICLE_PERMALINK)); + + final JSONObject nextArticle = articleRepository.getNextArticle(previousArticle.getString(Keys.OBJECT_ID)); + Assert.assertNotNull(previousArticle); + Assert.assertEquals(nextArticle.getString(Article.ARTICLE_TITLE), "article title2"); + } + + /** + * Get Most Comment Articles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "previousAndNext"}) + public void getMostCommentArticles() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + final JSONObject article = new JSONObject(); + + article.put(Article.ARTICLE_TITLE, "article title3"); + article.put(Article.ARTICLE_ABSTRACT, "article abstract"); + article.put(Article.ARTICLE_ABSTRACT_TEXT, "article abstract text"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2"); + article.put(Article.ARTICLE_AUTHOR_ID, "1"); + article.put(Article.ARTICLE_COMMENT_COUNT, 2); + article.put(Article.ARTICLE_VIEW_COUNT, 2); + article.put(Article.ARTICLE_CONTENT, "article content"); + article.put(Article.ARTICLE_PERMALINK, "article permalink3"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Article.ARTICLE_PUT_TOP, false); + article.put(Article.ARTICLE_CREATED, new Date().getTime()); + article.put(Article.ARTICLE_UPDATED, new Date().getTime()); + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + article.put(Article.ARTICLE_IMG1_URL, Article.getArticleImg1URL(article)); + + final Transaction transaction = articleRepository.beginTransaction(); + articleRepository.add(article); + transaction.commit(); + + List mostCommentArticles = articleRepository.getMostCommentArticles(2); + Assert.assertNotNull(mostCommentArticles); + Assert.assertEquals(mostCommentArticles.size(), 2); + Assert.assertEquals(mostCommentArticles.get(0).getInt(Article.ARTICLE_COMMENT_COUNT), 2); + Assert.assertEquals(mostCommentArticles.get(1).getInt(Article.ARTICLE_COMMENT_COUNT), 1); + + mostCommentArticles = articleRepository.getMostCommentArticles(1); + Assert.assertNotNull(mostCommentArticles); + Assert.assertEquals(mostCommentArticles.size(), 1); + Assert.assertEquals(mostCommentArticles.get(0).getInt(Article.ARTICLE_COMMENT_COUNT), 2); + } + + /** + * Get Most View Count Articles + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "previousAndNext", "getMostCommentArticles"}) + public void getMostViewCountArticles() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + final JSONObject article = new JSONObject(); + + article.put(Article.ARTICLE_TITLE, "article title4"); + article.put(Article.ARTICLE_ABSTRACT, "article abstract"); + article.put(Article.ARTICLE_ABSTRACT_TEXT, "article abstract text"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2"); + article.put(Article.ARTICLE_AUTHOR_ID, "1"); + article.put(Article.ARTICLE_COMMENT_COUNT, 3); + article.put(Article.ARTICLE_VIEW_COUNT, 3); + article.put(Article.ARTICLE_CONTENT, "article content"); + article.put(Article.ARTICLE_PERMALINK, "article permalink4"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_DRAFT); + article.put(Article.ARTICLE_PUT_TOP, false); + article.put(Article.ARTICLE_CREATED, new Date().getTime()); + article.put(Article.ARTICLE_UPDATED, new Date().getTime()); + article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random()); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + article.put(Article.ARTICLE_IMG1_URL, Article.getArticleImg1URL(article)); + + final Transaction transaction = articleRepository.beginTransaction(); + articleRepository.add(article); + transaction.commit(); + + List mostViewCountArticles = articleRepository.getMostViewCountArticles(2); + Assert.assertNotNull(mostViewCountArticles); + Assert.assertEquals(mostViewCountArticles.size(), 2); + Assert.assertEquals(mostViewCountArticles.get(0).getInt(Article.ARTICLE_VIEW_COUNT), 2); + Assert.assertEquals(mostViewCountArticles.get(1).getInt(Article.ARTICLE_VIEW_COUNT), 1); + + mostViewCountArticles = articleRepository.getMostViewCountArticles(1); + Assert.assertNotNull(mostViewCountArticles); + Assert.assertEquals(mostViewCountArticles.size(), 1); + Assert.assertEquals(mostViewCountArticles.get(0).getInt(Article.ARTICLE_VIEW_COUNT), 2); + + } + + /** + * Get Randomly. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "previousAndNext", "getMostCommentArticles", "getMostViewCountArticles"}) + public void getRandomly() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + List articles = articleRepository.getRandomly(3); + Assert.assertNotNull(articles); + } + + /** + * Get Recent Articles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "previousAndNext", "getMostCommentArticles", "getMostViewCountArticles"}) + public void getRecentArticles() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + Assert.assertEquals(articleRepository.count(), 4); + + List recentArticles = articleRepository.getRecentArticles(3); + Assert.assertNotNull(recentArticles); + Assert.assertEquals(recentArticles.size(), 3); + + Assert.assertEquals(recentArticles.get(0).getString(Article.ARTICLE_TITLE), "article title3"); + Assert.assertEquals(recentArticles.get(1).getString(Article.ARTICLE_TITLE), "article title2"); + Assert.assertEquals(recentArticles.get(2).getString(Article.ARTICLE_TITLE), "article title1"); + } + + /** + * Is Published. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "getMostViewCountArticles"}) + public void isPublished() throws Exception { + final ArticleRepository articleRepository = getArticleRepository(); + + final JSONArray all = articleRepository.get(new Query()).getJSONArray(Keys.RESULTS); + Assert.assertNotNull(all); + + final JSONObject article = all.getJSONObject(0); + Assert.assertTrue(articleRepository.isPublished(article.getString(Keys.OBJECT_ID))); + + final JSONObject notPublished = articleRepository.getByPermalink("article permalink4"); + Assert.assertNotNull(notPublished); + Assert.assertEquals(Article.ARTICLE_STATUS_C_DRAFT, notPublished.optInt(Article.ARTICLE_STATUS)); + + Assert.assertFalse(articleRepository.isPublished("not found")); + } +} diff --git a/src/test/java/org/b3log/solo/repository/CategoryRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/CategoryRepositoryImplTestCase.java new file mode 100644 index 00000000..a653e056 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/CategoryRepositoryImplTestCase.java @@ -0,0 +1,100 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Category; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link CategoryRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Apr 12, 2017 + * @since 2.0.0 + */ +@Test(suiteName = "repository") +public final class CategoryRepositoryImplTestCase extends AbstractTestCase { + + /** + * Tests. + * + * @throws Exception exception + */ + @Test + public void test() throws Exception { + final CategoryRepository categoryRepository = getCategoryRepository(); + + final int category1Order = 1, category2Order = 2, category3Order = 3; + + JSONObject category1 = new JSONObject(); + category1.put(Category.CATEGORY_TITLE, "category title"); + category1.put(Category.CATEGORY_DESCRIPTION, "cateogry description"); + category1.put(Category.CATEGORY_URI, "category uri"); + category1.put(Category.CATEGORY_ORDER, category1Order); + category1.put(Category.CATEGORY_TAG_CNT, 0); + + Transaction transaction = categoryRepository.beginTransaction(); + categoryRepository.add(category1); + transaction.commit(); + + Assert.assertNull(categoryRepository.getByTitle("title")); + Assert.assertNotNull(categoryRepository.getByTitle("category title")); + + Assert.assertNull(categoryRepository.getByOrder(0)); + Assert.assertNotNull(categoryRepository.getByOrder(category1Order)); + + final JSONObject category2 = new JSONObject(); + category2.put(Category.CATEGORY_TITLE, "category title"); + category2.put(Category.CATEGORY_DESCRIPTION, "cateogry description"); + category2.put(Category.CATEGORY_URI, "category uri"); + category2.put(Category.CATEGORY_ORDER, category2Order); + category2.put(Category.CATEGORY_TAG_CNT, 0); + + transaction = categoryRepository.beginTransaction(); + final String category2Id = categoryRepository.add(category2); + transaction.commit(); + + Assert.assertEquals(categoryRepository.getMaxOrder(), category2Order); + + JSONObject category3 = new JSONObject(); + category3.put(Category.CATEGORY_TITLE, "category title"); + category3.put(Category.CATEGORY_DESCRIPTION, "cateogry description"); + category3.put(Category.CATEGORY_URI, "category uri"); + category3.put(Category.CATEGORY_ORDER, category3Order); + category3.put(Category.CATEGORY_TAG_CNT, 0); + + transaction = categoryRepository.beginTransaction(); + categoryRepository.add(category3); + transaction.commit(); + + final int total = 3; + Assert.assertEquals(categoryRepository.count(), total); + + category1 = categoryRepository.getUpper(category2Id); + Assert.assertNotNull(category1); + Assert.assertEquals(category1.getInt(Category.CATEGORY_ORDER), category1Order); + + category3 = categoryRepository.getUnder(category2Id); + Assert.assertNotNull(category3); + Assert.assertEquals(category3.getInt(Category.CATEGORY_ORDER), category3Order); + } +} diff --git a/src/test/java/org/b3log/solo/repository/CommentRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/CommentRepositoryImplTestCase.java new file mode 100644 index 00000000..dacbee83 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/CommentRepositoryImplTestCase.java @@ -0,0 +1,70 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Comment; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.List; + +/** + * {@link ArticleRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.3, Apr 19, 2019 + */ +@Test(suiteName = "repository") +public class CommentRepositoryImplTestCase extends AbstractTestCase { + + /** + * Adds successfully. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final CommentRepository commentRepository = getCommentRepository(); + + final JSONObject comment = new JSONObject(); + + comment.put(Comment.COMMENT_CONTENT, "comment1 content"); + comment.put(Comment.COMMENT_CREATED, new Date().getTime()); + comment.put(Comment.COMMENT_NAME, "comment1 name"); + comment.put(Comment.COMMENT_ON_ID, "comment1 on id"); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, ""); + comment.put(Comment.COMMENT_ORIGINAL_COMMENT_NAME, ""); + comment.put(Comment.COMMENT_SHARP_URL, "comment1 sharp url"); + comment.put(Comment.COMMENT_URL, "comment1 url"); + comment.put(Comment.COMMENT_THUMBNAIL_URL, "comment1 thumbnail url"); + + final Transaction transaction = commentRepository.beginTransaction(); + commentRepository.add(comment); + transaction.commit(); + + final List comments = commentRepository.getComments("comment1 on id", 1, Integer.MAX_VALUE); + Assert.assertNotNull(comments); + Assert.assertEquals(comments.size(), 1); + + Assert.assertEquals(commentRepository.getComments("not found", 1, Integer.MAX_VALUE).size(), 0); + } +} diff --git a/src/test/java/org/b3log/solo/repository/LinkRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/LinkRepositoryImplTestCase.java new file mode 100644 index 00000000..1966d3e0 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/LinkRepositoryImplTestCase.java @@ -0,0 +1,101 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Link; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link LinkRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 29, 2011 + */ +@Test(suiteName = "repository") +public final class LinkRepositoryImplTestCase extends AbstractTestCase { + + /** + * Tests. + * + * @throws Exception exception + */ + @Test + public void test() throws Exception { + final LinkRepository linkRepository = getLinkRepository(); + + final int link1Order = 1, link2Order = 2, link3Order = 3; + + JSONObject link1 = new JSONObject(); + + link1.put(Link.LINK_TITLE, "link title"); + link1.put(Link.LINK_DESCRIPTION, "link description"); + link1.put(Link.LINK_ADDRESS, "link address"); + link1.put(Link.LINK_ORDER, link1Order); + + Transaction transaction = linkRepository.beginTransaction(); + linkRepository.add(link1); + transaction.commit(); + + Assert.assertNull(linkRepository.getByAddress("test")); + Assert.assertNotNull(linkRepository.getByAddress("link address")); + + Assert.assertNull(linkRepository.getByOrder(0)); + Assert.assertNotNull(linkRepository.getByOrder(link1Order)); + + final JSONObject link2 = new JSONObject(); + + link2.put(Link.LINK_TITLE, "link title"); + link2.put(Link.LINK_DESCRIPTION, "link description"); + link2.put(Link.LINK_ADDRESS, "link address"); + link2.put(Link.LINK_ORDER, link2Order); + + transaction = linkRepository.beginTransaction(); + final String link2Id = linkRepository.add(link2); + transaction.commit(); + + Assert.assertEquals(linkRepository.getMaxOrder(), link2Order); + + + JSONObject link3 = new JSONObject(); + + link3.put(Link.LINK_TITLE, "link title"); + link3.put(Link.LINK_DESCRIPTION, "link description"); + link3.put(Link.LINK_ADDRESS, "link address"); + link3.put(Link.LINK_ORDER, link3Order); + transaction = linkRepository.beginTransaction(); + + linkRepository.add(link3); + + transaction.commit(); + + final int total = 3; + Assert.assertEquals(linkRepository.count(), total); + + link1 = linkRepository.getUpper(link2Id); + Assert.assertNotNull(link1); + Assert.assertEquals(link1.getInt(Link.LINK_ORDER), link1Order); + + link3 = linkRepository.getUnder(link2Id); + Assert.assertNotNull(link3); + Assert.assertEquals(link3.getInt(Link.LINK_ORDER), link3Order); + } +} diff --git a/src/test/java/org/b3log/solo/repository/OptionRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/OptionRepositoryImplTestCase.java new file mode 100644 index 00000000..f634e897 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/OptionRepositoryImplTestCase.java @@ -0,0 +1,59 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link OptionRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Jan 29, 2019 + * @since 0.6.0 + */ +@Test(suiteName = "repository") +public final class OptionRepositoryImplTestCase extends AbstractTestCase { + + /** + * Tests. + * + * @throws Exception exception + */ + @Test + public void test() throws Exception { + final OptionRepository optionRepository = getOptionRepository(); + + final JSONObject option = new JSONObject(); + option.put(Keys.OBJECT_ID, Option.ID_C_BLOG_TITLE); + option.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + option.put(Option.OPTION_VALUE, 0L); + + Transaction transaction = optionRepository.beginTransaction(); + optionRepository.add(option); + transaction.commit(); + + Assert.assertEquals(optionRepository.count(), 1); + Assert.assertNotNull(optionRepository.get(Option.ID_C_BLOG_TITLE)); + } +} diff --git a/src/test/java/org/b3log/solo/repository/PageRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/PageRepositoryImplTestCase.java new file mode 100644 index 00000000..736c5152 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/PageRepositoryImplTestCase.java @@ -0,0 +1,158 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Page; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link PageRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.4, Apr 19, 2019 + */ +@Test(suiteName = "repository") +public class PageRepositoryImplTestCase extends AbstractTestCase { + + /** + * Adds successfully. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final PageRepository pageRepository = getPageRepository(); + + Assert.assertEquals(pageRepository.getMaxOrder(), -1); + + final JSONObject page = new JSONObject(); + page.put(Page.PAGE_ORDER, 0); + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p1"); + page.put(Page.PAGE_TITLE, "page1 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + page.put(Page.PAGE_ICON, ""); + + final Transaction transaction = pageRepository.beginTransaction(); + pageRepository.add(page); + transaction.commit(); + + final List pages = pageRepository.getPages(); + Assert.assertNotNull(pages); + Assert.assertEquals(pages.size(), 1); + } + + /** + * Get By Permalink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByPermalink() throws Exception { + final PageRepository pageRepository = getPageRepository(); + + final JSONObject page1 = pageRepository.getByPermalink(Latkes.getServePath() + "/p1"); + Assert.assertNotNull(page1); + Assert.assertEquals(page1.getString(Page.PAGE_TITLE), "page1 title"); + + Assert.assertNull(pageRepository.getByPermalink("not found")); + } + + /** + * Get Max Order. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getMaxOrder() throws Exception { + final PageRepository pageRepository = getPageRepository(); + + final JSONObject page = new JSONObject(); + page.put(Page.PAGE_ORDER, 1); + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p2"); + page.put(Page.PAGE_TITLE, "page2 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + page.put(Page.PAGE_ICON, ""); + + final Transaction transaction = pageRepository.beginTransaction(); + pageRepository.add(page); + transaction.commit(); + + final int maxOrder = pageRepository.getMaxOrder(); + Assert.assertEquals(maxOrder, 1); + } + + /** + * Get Under and Upper. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "getMaxOrder"}) + public void getUnderAndUpper() throws Exception { + final PageRepository pageRepository = getPageRepository(); + + final JSONObject page = new JSONObject(); + page.put(Page.PAGE_ORDER, 2); + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p3"); + page.put(Page.PAGE_TITLE, "page3 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + page.put(Page.PAGE_ICON, ""); + + final Transaction transaction = pageRepository.beginTransaction(); + pageRepository.add(page); + transaction.commit(); + + final JSONObject page2 = pageRepository.getByPermalink(Latkes.getServePath() + "/p2"); + Assert.assertNotNull(page2); + + final JSONObject page1 = pageRepository.getUpper(page2.getString(Keys.OBJECT_ID)); + Assert.assertNotNull(page1); + + final JSONObject page3 = pageRepository.getUnder(page2.getString(Keys.OBJECT_ID)); + Assert.assertNotNull(page3); + + final JSONObject notFound = pageRepository.getUpper(page1.getString(Keys.OBJECT_ID)); + Assert.assertNull(notFound); + + Assert.assertNull(pageRepository.getUpper("not found")); + Assert.assertNull(pageRepository.getUnder("not found")); + } + + /** + * Get By Order. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = {"add", "getMaxOrder"}) + public void getByOrder() throws Exception { + final PageRepository pageRepository = getPageRepository(); + + final JSONObject page1 = pageRepository.getByOrder(0); + Assert.assertNotNull(page1); + Assert.assertEquals(page1.getString(Page.PAGE_TITLE), "page1 title"); + + Assert.assertNull(pageRepository.getByOrder(Integer.MIN_VALUE)); + } +} diff --git a/src/test/java/org/b3log/solo/repository/PluginRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/PluginRepositoryImplTestCase.java new file mode 100644 index 00000000..12cd25fe --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/PluginRepositoryImplTestCase.java @@ -0,0 +1,41 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.solo.AbstractTestCase; +import org.testng.annotations.Test; + +/** + * {@link PluginRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Dec 31, 2011 + */ +@Test(suiteName = "repository") +public class PluginRepositoryImplTestCase extends AbstractTestCase { + + /** + * Tests. + * + * @throws Exception exception + */ + @Test + public void test() throws Exception { + final PluginRepository pluginRepository = getPluginRepository(); + } +} diff --git a/src/test/java/org/b3log/solo/repository/TagArticleRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/TagArticleRepositoryImplTestCase.java new file mode 100644 index 00000000..bfce51c6 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/TagArticleRepositoryImplTestCase.java @@ -0,0 +1,100 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import junit.framework.Assert; +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Tag; +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link TagArticleRepository} test case. + * + * @author Liang Ding + * @version 1.1.0.0, Jan 28, 2019 + */ +@Test(suiteName = "repository") +public class TagArticleRepositoryImplTestCase extends AbstractTestCase { + + /** + * Add. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final TagArticleRepository tagArticleRepository = getTagArticleRepository(); + + final JSONObject tagArticle = new JSONObject(); + tagArticle.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, "article1 id"); + tagArticle.put(Tag.TAG + "_" + Keys.OBJECT_ID, "tag1 id"); + + final Transaction transaction = tagArticleRepository.beginTransaction(); + tagArticleRepository.add(tagArticle); + transaction.commit(); + } + + /** + * Get By ArticleId. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByArticleId() throws Exception { + final TagArticleRepository tagArticleRepository = getTagArticleRepository(); + + final List tagArticle = tagArticleRepository.getByArticleId("article1 id"); + Assert.assertNotNull(tagArticle); + + Assert.assertEquals(0, tagArticleRepository.getByArticleId("").size()); + } + + /** + * Get By TagId. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByTagId() throws Exception { + final TagArticleRepository tagArticleRepository = getTagArticleRepository(); + + final JSONArray results = tagArticleRepository.getByTagId("tag1 id", 1, Integer.MAX_VALUE).getJSONArray(Keys.RESULTS); + Assert.assertEquals(1, results.length()); + } + + /** + * Get Most Used Tags. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getMostUsedTags() throws Exception { + final TagArticleRepository tagArticleRepository = getTagArticleRepository(); + + final List mostUsedTags = tagArticleRepository.getMostUsedTags(3); + Assert.assertNotNull(mostUsedTags); + Assert.assertEquals(1, mostUsedTags.size()); + } +} diff --git a/src/test/java/org/b3log/solo/repository/TagRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/TagRepositoryImplTestCase.java new file mode 100644 index 00000000..1a678816 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/TagRepositoryImplTestCase.java @@ -0,0 +1,107 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import junit.framework.Assert; +import org.b3log.latke.Keys; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Tag; +import org.json.JSONObject; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link TagRepository} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Jan 28, 2019 + */ +@Test(suiteName = "repository") +public class TagRepositoryImplTestCase extends AbstractTestCase { + + /** + * Add. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final TagRepository tagRepository = getTagRepository(); + + final JSONObject tag = new JSONObject(); + tag.put(Tag.TAG_TITLE, "tag title1"); + + final Transaction transaction = tagRepository.beginTransaction(); + tagRepository.add(tag); + transaction.commit(); + } + + /** + * Get By Title. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByTitle() throws Exception { + final TagRepository tagRepository = getTagRepository(); + + final JSONObject found = tagRepository.getByTitle("tag title1"); + Assert.assertNotNull(found); + Assert.assertEquals(found.getString(Tag.TAG_TITLE), "tag title1"); + + final JSONObject notFound = tagRepository.getByTitle(""); + Assert.assertNull(notFound); + } + + /** + * Get By ArticleId. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "add") + public void getByArticleId() throws Exception { + addTagArticle(); + + final TagRepository tagRepository = getTagRepository(); + + List tags = tagRepository.getByArticleId("article1 id"); + Assert.assertNotNull(tags); + Assert.assertEquals(1, tags.size()); + + tags = tagRepository.getByArticleId("not found"); + Assert.assertNotNull(tags); + Assert.assertEquals(0, tags.size()); + } + + private void addTagArticle() throws Exception { + final TagArticleRepository tagArticleRepository + = getTagArticleRepository(); + + final JSONObject tagArticle = new JSONObject(); + + tagArticle.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, "article1 id"); + tagArticle.put(Tag.TAG + "_" + Keys.OBJECT_ID, "tag1 id"); + + final Transaction transaction = tagArticleRepository.beginTransaction(); + tagArticleRepository.add(tagArticle); + transaction.commit(); + } +} diff --git a/src/test/java/org/b3log/solo/repository/UserRepositoryImplTestCase.java b/src/test/java/org/b3log/solo/repository/UserRepositoryImplTestCase.java new file mode 100644 index 00000000..34ff1ab9 --- /dev/null +++ b/src/test/java/org/b3log/solo/repository/UserRepositoryImplTestCase.java @@ -0,0 +1,97 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.repository; + +import org.b3log.latke.Keys; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.repository.FilterOperator; +import org.b3log.latke.repository.PropertyFilter; +import org.b3log.latke.repository.Query; +import org.b3log.latke.repository.Transaction; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.UserExt; +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link UserRepository} test case. + * + * @author Liang Ding + * @version 1.1.0.3, Feb 8, 2019 + */ +@Test(suiteName = "repository") +public final class UserRepositoryImplTestCase extends AbstractTestCase { + + /** + * Tests. + * + * @throws Exception exception + */ + @Test + public void test() throws Exception { + final UserRepository userRepository = getUserRepository(); + + final JSONObject another = new JSONObject(); + another.put(User.USER_NAME, "test1"); + another.put(User.USER_URL, "https://b3log.org"); + another.put(User.USER_ROLE, Role.DEFAULT_ROLE); + another.put(UserExt.USER_AVATAR, ""); + another.put(UserExt.USER_GITHUB_ID, ""); + another.put(UserExt.USER_B3_KEY, ""); + + Transaction transaction = userRepository.beginTransaction(); + userRepository.add(another); + transaction.commit(); + + Assert.assertNull(userRepository.getAdmin()); + + JSONObject admin = new JSONObject(); + admin.put(User.USER_NAME, "test"); + admin.put(User.USER_URL, "https://b3log.org"); + admin.put(User.USER_ROLE, Role.ADMIN_ROLE); + admin.put(UserExt.USER_AVATAR, ""); + admin.put(UserExt.USER_GITHUB_ID, ""); + admin.put(UserExt.USER_B3_KEY, ""); + + transaction = userRepository.beginTransaction(); + userRepository.add(admin); + transaction.commit(); + + admin = userRepository.getAdmin(); + + Assert.assertNotNull(admin); + Assert.assertEquals("test", admin.optString(User.USER_NAME)); + + final JSONObject result = userRepository.get(new Query().setFilter( + new PropertyFilter(User.USER_NAME, FilterOperator.EQUAL, "test1"))); + + final JSONArray users = result.getJSONArray(Keys.RESULTS); + Assert.assertEquals(users.length(), 1); + Assert.assertEquals(users.getJSONObject(0).getString(User.USER_NAME), "test1"); + + final JSONObject notFound = userRepository.getByUserName("not.found"); + Assert.assertNull(notFound); + + final JSONObject found = userRepository.getByUserName("test1"); + Assert.assertNotNull(found); + Assert.assertEquals(found.getString(User.USER_NAME), "test1"); + } +} diff --git a/src/test/java/org/b3log/solo/service/ArchiveDateQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/ArchiveDateQueryServiceTestCase.java new file mode 100644 index 00000000..ef43648b --- /dev/null +++ b/src/test/java/org/b3log/solo/service/ArchiveDateQueryServiceTestCase.java @@ -0,0 +1,79 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.apache.commons.lang.time.DateFormatUtils; +import org.apache.commons.lang.time.DateUtils; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.ArchiveDate; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.List; + +/** + * {@link ArchiveDateQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 11, 2012 + */ +public class ArchiveDateQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Get Archive Dates. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArchiveDates() throws Exception { + final ArchiveDateQueryService archiveDateQueryService = getArchiveDateQueryService(); + + final List archiveDates = archiveDateQueryService.getArchiveDates(); + + Assert.assertNotNull(archiveDates); + Assert.assertEquals(archiveDates.size(), 1); + } + + /** + * Get By Archive Date String. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getByArchiveDateString() throws Exception { + final ArchiveDateQueryService archiveDateQueryService = getArchiveDateQueryService(); + + final String archiveDateString = DateFormatUtils.format(new Date(), "yyyy/MM"); + final JSONObject result = archiveDateQueryService.getByArchiveDateString(archiveDateString); + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(ArchiveDate.ARCHIVE_DATE).getLong(ArchiveDate.ARCHIVE_TIME), + DateUtils.parseDate(archiveDateString, new String[]{"yyyy/MM"}).getTime()); + } +} diff --git a/src/test/java/org/b3log/solo/service/ArticleMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/ArticleMgmtServiceTestCase.java new file mode 100644 index 00000000..587d9fbd --- /dev/null +++ b/src/test/java/org/b3log/solo/service/ArticleMgmtServiceTestCase.java @@ -0,0 +1,283 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Common; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link ArticleMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.6, Sep 16, 2018 + */ +@Test(suiteName = "service") +public class ArticleMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add Article. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addArticle() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject article = new JSONObject(); + requestJSONObject.put(Article.ARTICLE, article); + + final JSONObject admin = getUserQueryService().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + article.put(Article.ARTICLE_AUTHOR_ID, userId); + article.put(Article.ARTICLE_TITLE, "article1 title"); + article.put(Article.ARTICLE_ABSTRACT, "article1 abstract"); + article.put(Article.ARTICLE_CONTENT, "article1 content"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2, tag3"); + article.put(Article.ARTICLE_PERMALINK, "article1 permalink"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, true); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + + final String articleId = articleMgmtService.addArticle(requestJSONObject); + + Assert.assertNotNull(articleId); + } + + /** + * Add Article without permalink. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addArticleWithoutPermalink() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject article = new JSONObject(); + requestJSONObject.put(Article.ARTICLE, article); + + final JSONObject admin = getUserQueryService().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + article.put(Article.ARTICLE_AUTHOR_ID, userId); + article.put(Article.ARTICLE_TITLE, "article1 title"); + article.put(Article.ARTICLE_ABSTRACT, "article1 abstract"); + article.put(Article.ARTICLE_CONTENT, "article1 content"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2, tag3"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, true); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + + final String articleId = articleMgmtService.addArticle(requestJSONObject); + + Assert.assertNotNull(articleId); + } + + /** + * Update Article. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updateArticle() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject article = new JSONObject(); + requestJSONObject.put(Article.ARTICLE, article); + + final JSONObject admin = getUserQueryService().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + article.put(Article.ARTICLE_AUTHOR_ID, userId); + article.put(Article.ARTICLE_TITLE, "article2 title"); + article.put(Article.ARTICLE_ABSTRACT, "article2 abstract"); + article.put(Article.ARTICLE_CONTENT, "article2 content"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2, tag3"); + article.put(Article.ARTICLE_PERMALINK, "article2 permalink"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, true); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + + final String articleId = articleMgmtService.addArticle(requestJSONObject); + + Assert.assertNotNull(articleId); + + article.put(Keys.OBJECT_ID, articleId); + article.put(Article.ARTICLE_TITLE, "updated article2 title"); + + articleMgmtService.updateArticle(requestJSONObject); + + final ArticleQueryService articleQueryService = getArticleQueryService(); + final JSONObject updated = articleQueryService.getArticleById(articleId); + Assert.assertNotNull(updated); + Assert.assertEquals(updated.getString(Article.ARTICLE_TITLE), "updated article2 title"); + } + + /** + * Remove Article. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removeArticle() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject article = new JSONObject(); + requestJSONObject.put(Article.ARTICLE, article); + + final JSONObject admin = getUserQueryService().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + article.put(Article.ARTICLE_AUTHOR_ID, userId); + article.put(Article.ARTICLE_TITLE, "article3 title"); + article.put(Article.ARTICLE_ABSTRACT, "article3 abstract"); + article.put(Article.ARTICLE_CONTENT, "article3 content"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2, tag3"); + article.put(Article.ARTICLE_PERMALINK, "article3 permalink"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, true); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + + final String articleId = articleMgmtService.addArticle(requestJSONObject); + + Assert.assertNotNull(articleId); + + articleMgmtService.removeArticle(articleId); + + final ArticleQueryService articleQueryService = getArticleQueryService(); + final JSONObject updated = articleQueryService.getArticleById(articleId); + Assert.assertNull(updated); + } + + /** + * Top Article. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addArticle") + public void topArticle() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + final ArticleQueryService articleQueryService = getArticleQueryService(); + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + final JSONArray articles = articleQueryService.getArticles(paginationRequest).optJSONArray(Article.ARTICLES); + + Assert.assertNotEquals(articles.length(), 0); + final JSONObject article = articles.getJSONObject(0); + + final String articleId = article.getString(Keys.OBJECT_ID); + articleMgmtService.topArticle(articleId, true); + articleMgmtService.topArticle(articleId, false); + } + + /** + * Cancel Publish Article. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void cancelPublishArticle() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject article = new JSONObject(); + requestJSONObject.put(Article.ARTICLE, article); + + final JSONObject admin = getUserQueryService().getAdmin(); + final String userId = admin.optString(Keys.OBJECT_ID); + + article.put(Article.ARTICLE_AUTHOR_ID, userId); + article.put(Article.ARTICLE_TITLE, "article4 title"); + article.put(Article.ARTICLE_ABSTRACT, "article4 abstract"); + article.put(Article.ARTICLE_CONTENT, "article4 content"); + article.put(Article.ARTICLE_TAGS_REF, "tag1, tag2, tag3"); + article.put(Article.ARTICLE_PERMALINK, "article4 permalink"); + article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED); + article.put(Common.POST_TO_COMMUNITY, true); + article.put(Article.ARTICLE_SIGN_ID, "1"); + article.put(Article.ARTICLE_COMMENTABLE, true); + article.put(Article.ARTICLE_VIEW_PWD, ""); + + final String articleId = articleMgmtService.addArticle(requestJSONObject); + + Assert.assertNotNull(articleId); + + final ArticleQueryService articleQueryService = getArticleQueryService(); + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + JSONArray articles = articleQueryService.getArticles(paginationRequest).optJSONArray(Article.ARTICLES); + + int articleCount = articles.length(); + Assert.assertNotEquals(articleCount, 0); + + articleMgmtService.cancelPublishArticle(articleId); + articles = articleQueryService.getArticles(paginationRequest).optJSONArray(Article.ARTICLES); + Assert.assertEquals(articles.length(), articleCount - 1); + } + + /** + * Update Articles Random Value. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addArticle") + public void updateArticlesRandomValue() throws Exception { + final ArticleMgmtService articleMgmtService = getArticleMgmtService(); + final ArticleQueryService articleQueryService = getArticleQueryService(); + + List articles = articleQueryService.getRecentArticles(10); + Assert.assertNotEquals(articles.size(), 0); + + final JSONObject article = articles.get(0); + final String articleId = article.getString(Keys.OBJECT_ID); + double randomValue = article.getDouble(Article.ARTICLE_RANDOM_DOUBLE); + articleMgmtService.updateArticlesRandomValue(Integer.MAX_VALUE); + + //Assert.assertNotEquals(articleQueryService.getArticleById(articleId). + // getDouble(Article.ARTICLE_RANDOM_DOUBLE), randomValue); + } +} diff --git a/src/test/java/org/b3log/solo/service/ArticleQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/ArticleQueryServiceTestCase.java new file mode 100644 index 00000000..c873ee3b --- /dev/null +++ b/src/test/java/org/b3log/solo/service/ArticleQueryServiceTestCase.java @@ -0,0 +1,187 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Tag; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link ArticleQueryService} test case. + * + * @author Liang Ding + * @version 1.1.0.2, Jan 28, 2019 + */ +@Test(suiteName = "service") +public class ArticleQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Search articles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void searchKeyword() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + + JSONObject result = articleQueryService.searchKeyword("初始化", 1, 20); + Assert.assertNotNull(result); + List articles = (List) result.opt(Article.ARTICLES); + Assert.assertEquals(articles.size(), 1); + + result = articleQueryService.searchKeyword("不存在的", 1, 20); + Assert.assertNotNull(result); + articles = (List) result.opt(Article.ARTICLES); + Assert.assertEquals(articles.size(), 0); + } + + /** + * Get Recent Articles. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getRecentArticles() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + final List articles = articleQueryService.getRecentArticles(10); + + Assert.assertEquals(articles.size(), 1); + } + + /** + * Get Article. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getRecentArticles") + public void getArticle() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + final List articles = articleQueryService.getRecentArticles(10); + + Assert.assertEquals(articles.size(), 1); + + final String articleId = articles.get(0).getString(Keys.OBJECT_ID); + final JSONObject article = articleQueryService.getArticle(articleId); + + Assert.assertNotNull(article); + Assert.assertEquals(article.optString(Article.ARTICLE_VIEW_COUNT), ""); + } + + /** + * Get Article By Id. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getRecentArticles") + public void getArticleById() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + final List articles = articleQueryService.getRecentArticles(10); + + Assert.assertEquals(articles.size(), 1); + + final String articleId = articles.get(0).getString(Keys.OBJECT_ID); + final JSONObject article = articleQueryService.getArticleById(articleId); + + Assert.assertNotNull(article); + } + + /** + * Get Article Content. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "getRecentArticles") + public void getArticleContent() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + + final List articles = articleQueryService.getRecentArticles(10); + + Assert.assertEquals(articles.size(), 1); + + final String articleId = articles.get(0).getString(Keys.OBJECT_ID); + + Assert.assertNotNull(articleQueryService.getArticleContent(null, articleId)); + } + + /** + * Get Articles By Tag. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticlesByTag() throws Exception { + final TagQueryService tagQueryService = getTagQueryService(); + + JSONObject result = tagQueryService.getTagByTitle("Solo"); + Assert.assertNotNull(result); + + final JSONObject tag = result.getJSONObject(Tag.TAG); + Assert.assertNotNull(tag); + + final String tagId = tag.getString(Keys.OBJECT_ID); + + final ArticleQueryService articleQueryService = getArticleQueryService(); + final JSONObject articlesResult = articleQueryService.getArticlesByTag(tagId, 1, Integer.MAX_VALUE); + Assert.assertNotNull(articlesResult); + final List articles = (List) articlesResult.opt(Keys.RESULTS); + Assert.assertEquals(articles.size(), 1); + } + + /** + * Get Archives By Archive Date. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getArticlesByArchiveDate() throws Exception { + final ArchiveDateQueryService archiveDateQueryService = getArchiveDateQueryService(); + + final List archiveDates = archiveDateQueryService.getArchiveDates(); + + Assert.assertNotNull(archiveDates); + Assert.assertEquals(archiveDates.size(), 1); + + final JSONObject archiveDate = archiveDates.get(0); + + final ArticleQueryService articleQueryService = getArticleQueryService(); + List articles = + articleQueryService.getArticlesByArchiveDate(archiveDate.getString(Keys.OBJECT_ID), 1, Integer.MAX_VALUE); + Assert.assertNotNull(articles); + Assert.assertEquals(articles.size(), 1); + + articles = articleQueryService.getArticlesByArchiveDate("not found", 1, Integer.MAX_VALUE); + Assert.assertNotNull(articles); + Assert.assertTrue(articles.isEmpty()); + } +} diff --git a/src/test/java/org/b3log/solo/service/CategoryMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/CategoryMgmtServiceTestCase.java new file mode 100644 index 00000000..4ed2eede --- /dev/null +++ b/src/test/java/org/b3log/solo/service/CategoryMgmtServiceTestCase.java @@ -0,0 +1,148 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Category; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link CategoryMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Apr 12, 2017 + * @since 2.0.0 + */ +@Test(suiteName = "service") +public class CategoryMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add a category. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addCategory() throws Exception { + final CategoryMgmtService categoryMgmtService = getCategoryMgmtService(); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, "category1 title"); + category.put(Category.CATEGORY_URI, "category1 uri"); + category.put(Category.CATEGORY_DESCRIPTION, "category1 description"); + + final String categoryId = categoryMgmtService.addCategory(category); + Assert.assertNotNull(categoryId); + } + + /** + * Remove a category. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removeCategory() throws Exception { + final CategoryMgmtService categoryMgmtService = getCategoryMgmtService(); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, "category2 title"); + category.put(Category.CATEGORY_URI, "category2 uri"); + category.put(Category.CATEGORY_DESCRIPTION, "category2 description"); + + final String categoryId = categoryMgmtService.addCategory(category); + Assert.assertNotNull(categoryId); + + final CategoryQueryService categoryQueryService = getCategoryQueryService(); + JSONObject result = categoryQueryService.getCategory(categoryId); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getString(Category.CATEGORY_TITLE), "category2 title"); + + categoryMgmtService.removeCategory(categoryId); + + result = categoryQueryService.getCategory(categoryId); + Assert.assertNull(result); + } + + /** + * Update a category. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updateCategory() throws Exception { + final CategoryMgmtService categoryMgmtService = getCategoryMgmtService(); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, "category3 title"); + category.put(Category.CATEGORY_URI, "category3 uri"); + category.put(Category.CATEGORY_DESCRIPTION, "category3 description"); + + final String categoryId = categoryMgmtService.addCategory(category); + Assert.assertNotNull(categoryId); + + final CategoryQueryService categoryQueryService = getCategoryQueryService(); + JSONObject result = categoryQueryService.getCategory(categoryId); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getString(Category.CATEGORY_TITLE), "category3 title"); + + category.put(Category.CATEGORY_TITLE, "updated category3 title"); + categoryMgmtService.updateCategory(categoryId, category); + + result = categoryQueryService.getCategory(categoryId); + Assert.assertNotNull(result); + Assert.assertEquals(result.getString(Category.CATEGORY_TITLE), "updated category3 title"); + } + + /** + * Change Order. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addCategory") + public void changeOrder() throws Exception { + final CategoryMgmtService categoryMgmtService = getCategoryMgmtService(); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, "category4 title"); + category.put(Category.CATEGORY_URI, "category4 uri"); + category.put(Category.CATEGORY_DESCRIPTION, "category4 description"); + + final String categoryId = categoryMgmtService.addCategory(category); + Assert.assertNotNull(categoryId); + + final int oldOrder = category.getInt(Category.CATEGORY_ORDER); + categoryMgmtService.changeOrder(categoryId, "up"); + + final JSONObject result = getCategoryQueryService().getCategory(categoryId); + Assert.assertNotNull(result); + Assert.assertTrue(oldOrder > result.getInt(Category.CATEGORY_ORDER)); + } +} diff --git a/src/test/java/org/b3log/solo/service/CategoryQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/CategoryQueryServiceTestCase.java new file mode 100644 index 00000000..9cea5aa3 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/CategoryQueryServiceTestCase.java @@ -0,0 +1,80 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Category; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link CategoryQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Apr 12, 2017 + * @since 2.0.0 + */ +@Test(suiteName = "service") +public class CategoryQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add a category. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addCategory() throws Exception { + final CategoryMgmtService categoryMgmtService = getCategoryMgmtService(); + + final JSONObject category = new JSONObject(); + category.put(Category.CATEGORY_TITLE, "category1 title"); + category.put(Category.CATEGORY_URI, "category1 uri"); + category.put(Category.CATEGORY_DESCRIPTION, "category1 description"); + + final String categoryId = categoryMgmtService.addCategory(category); + Assert.assertNotNull(categoryId); + } + + /** + * Get categories. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addCategory") + public void getCategories() throws Exception { + final CategoryQueryService categoryQueryService = getCategoryQueryService(); + + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + final JSONObject result = categoryQueryService.getCategoris(paginationRequest); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Category.CATEGORIES).length(), 1); + } +} diff --git a/src/test/java/org/b3log/solo/service/CommentMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/CommentMgmtServiceTestCase.java new file mode 100644 index 00000000..3b4da20e --- /dev/null +++ b/src/test/java/org/b3log/solo/service/CommentMgmtServiceTestCase.java @@ -0,0 +1,113 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.latke.Latkes; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Comment; +import org.b3log.solo.model.Page; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link CommentMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.4, Apr 18, 2019 + */ +@Test(suiteName = "service") +public class CommentMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add Article Comment. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addArticleComment() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + + final List articles = articleQueryService.getRecentArticles(10); + + Assert.assertEquals(articles.size(), 1); + + final CommentQueryService commentQueryService = getCommentQueryService(); + JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + JSONObject result = commentQueryService.getComments(paginationRequest); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Comment.COMMENTS).length(), 1); + + final CommentMgmtService commentMgmtService = getCommentMgmtService(); + final JSONObject requestJSONObject = new JSONObject(); + + final String articleId = articles.get(0).getString(Keys.OBJECT_ID); + requestJSONObject.put(Keys.OBJECT_ID, articleId); + requestJSONObject.put(Comment.COMMENT_NAME, "Solo"); + requestJSONObject.put(Comment.COMMENT_URL, "comment URL"); + requestJSONObject.put(Comment.COMMENT_CONTENT, "comment content"); + + final JSONObject addResult = commentMgmtService.addArticleComment(requestJSONObject); + Assert.assertNotNull(addResult); + Assert.assertNotNull(addResult.getString(Keys.OBJECT_ID)); + Assert.assertNotNull(addResult.getString(Comment.COMMENT_T_DATE)); + Assert.assertNotNull(addResult.getString(Comment.COMMENT_THUMBNAIL_URL)); + Assert.assertNotNull(addResult.getString(Comment.COMMENT_SHARP_URL)); + + result = commentQueryService.getComments(paginationRequest); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Comment.COMMENTS).length(), 2); + } + + /** + * Adds a page. + * + * @throws Exception exception + */ + private void addPage() throws Exception { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p1"); + page.put(Page.PAGE_TITLE, "page1 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + final String pageId = pageMgmtService.addPage(requestJSONObject); + + Assert.assertNotNull(pageId); + } +} diff --git a/src/test/java/org/b3log/solo/service/CommentQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/CommentQueryServiceTestCase.java new file mode 100644 index 00000000..59e539d5 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/CommentQueryServiceTestCase.java @@ -0,0 +1,92 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Article; +import org.b3log.solo.model.Comment; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link CommentQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 11, 2012 + */ +@Test(suiteName = "service") +public class CommentQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Get Comments. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getComments() throws Exception { + final CommentQueryService commentQueryService = getCommentQueryService(); + + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + final JSONObject result = commentQueryService.getComments(paginationRequest); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Comment.COMMENTS).length(), 1); + } + + /** + * Get Comment on id. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getCommentsOnId() throws Exception { + final ArticleQueryService articleQueryService = getArticleQueryService(); + final JSONObject result = articleQueryService.getArticles(Solos.buildPaginationRequest("1/10/20")); + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Article.ARTICLES).length(), 1); + + final JSONObject article = + result.getJSONArray(Article.ARTICLES).getJSONObject(0); + final String articleId = article.getString(Keys.OBJECT_ID); + + final CommentQueryService commentQueryService = getCommentQueryService(); + final List comments = + commentQueryService.getComments(articleId); + Assert.assertNotNull(comments); + Assert.assertEquals(comments.size(), 1); + } +} diff --git a/src/test/java/org/b3log/solo/service/LinkMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/LinkMgmtServiceTestCase.java new file mode 100644 index 00000000..5510cd2b --- /dev/null +++ b/src/test/java/org/b3log/solo/service/LinkMgmtServiceTestCase.java @@ -0,0 +1,163 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Link; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link LinkMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 11, 2012 + */ +@Test(suiteName = "service") +public class LinkMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add Link. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addLink() throws Exception { + final LinkMgmtService linkMgmtService = getLinkMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSONObject.put(Link.LINK, link); + + link.put(Link.LINK_TITLE, "link1 title"); + link.put(Link.LINK_ADDRESS, "link1 address"); + link.put(Link.LINK_DESCRIPTION, "link1 description"); + + final String linkId = linkMgmtService.addLink(requestJSONObject); + Assert.assertNotNull(linkId); + } + + /** + * Remove Link. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removeLink() throws Exception { + final LinkMgmtService linkMgmtService = getLinkMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSONObject.put(Link.LINK, link); + + link.put(Link.LINK_TITLE, "link2 title"); + link.put(Link.LINK_ADDRESS, "link2 address"); + link.put(Link.LINK_DESCRIPTION, "link2 description"); + + final String linkId = linkMgmtService.addLink(requestJSONObject); + Assert.assertNotNull(linkId); + + final LinkQueryService linkQueryService = getLinkQueryService(); + JSONObject result = linkQueryService.getLink(linkId); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(Link.LINK). + getString(Link.LINK_TITLE), "link2 title"); + + linkMgmtService.removeLink(linkId); + + result = linkQueryService.getLink(linkId); + Assert.assertNull(result); + } + + /** + * Update Link. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updateLink() throws Exception { + final LinkMgmtService linkMgmtService = getLinkMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSONObject.put(Link.LINK, link); + + link.put(Link.LINK_TITLE, "link3 title"); + link.put(Link.LINK_ADDRESS, "link3 address"); + link.put(Link.LINK_DESCRIPTION, "link3 description"); + + final String linkId = linkMgmtService.addLink(requestJSONObject); + Assert.assertNotNull(linkId); + + final LinkQueryService linkQueryService = getLinkQueryService(); + JSONObject result = linkQueryService.getLink(linkId); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(Link.LINK). + getString(Link.LINK_TITLE), "link3 title"); + + link.put(Link.LINK_TITLE, "updated link3 title"); + linkMgmtService.updateLink(requestJSONObject); + + result = linkQueryService.getLink(linkId); + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(Link.LINK).getString( + Link.LINK_TITLE), "updated link3 title"); + } + + /** + * Change Order. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addLink") + public void changeOrder() throws Exception { + final LinkMgmtService linkMgmtService = getLinkMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSONObject.put(Link.LINK, link); + + link.put(Link.LINK_TITLE, "link4 title"); + link.put(Link.LINK_ADDRESS, "link4 address"); + link.put(Link.LINK_DESCRIPTION, "link4 description"); + + final String linkId = linkMgmtService.addLink(requestJSONObject); + Assert.assertNotNull(linkId); + + final int oldOrder = link.getInt(Link.LINK_ORDER); + linkMgmtService.changeOrder(linkId, "up"); + + final JSONObject result = getLinkQueryService().getLink(linkId); + Assert.assertNotNull(result); + Assert.assertTrue(oldOrder > result.getJSONObject(Link.LINK).getInt( + Link.LINK_ORDER)); + } +} diff --git a/src/test/java/org/b3log/solo/service/LinkQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/LinkQueryServiceTestCase.java new file mode 100644 index 00000000..42108dc3 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/LinkQueryServiceTestCase.java @@ -0,0 +1,82 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Link; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link LinkQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.2, Nov 2, 2016 + */ +@Test(suiteName = "service") +public class LinkQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add Link. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addLink() throws Exception { + final LinkMgmtService linkMgmtService = getLinkMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject link = new JSONObject(); + requestJSONObject.put(Link.LINK, link); + + link.put(Link.LINK_TITLE, "link1 title"); + link.put(Link.LINK_ADDRESS, "link1 address"); + link.put(Link.LINK_DESCRIPTION, "link1 description"); + + final String linkId = linkMgmtService.addLink(requestJSONObject); + Assert.assertNotNull(linkId); + } + + /** + * Get Links. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addLink") + public void getLinks() throws Exception { + final LinkQueryService linkQueryService = getLinkQueryService(); + + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + final JSONObject result = linkQueryService.getLinks(paginationRequest); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Link.LINKS).length(), 2); + } +} diff --git a/src/test/java/org/b3log/solo/service/OptionMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/OptionMgmtServiceTestCase.java new file mode 100644 index 00000000..0201dd9b --- /dev/null +++ b/src/test/java/org/b3log/solo/service/OptionMgmtServiceTestCase.java @@ -0,0 +1,110 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Keys; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link OptionMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Jan 29, 2019 + * @since 0.6.0 + */ +@Test(suiteName = "service") +public class OptionMgmtServiceTestCase extends AbstractTestCase { + + /** + * Add. + * + * @throws Exception exception + */ + @Test + public void add() throws Exception { + final OptionMgmtService optionMgmtService = getOptionMgmtService(); + + final JSONObject option = new JSONObject(); + option.put(Keys.OBJECT_ID, Option.ID_C_BLOG_TITLE); + option.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + option.put(Option.OPTION_VALUE, 0L); + + final String id = optionMgmtService.addOrUpdateOption(option); + //System.out.println(id); + Assert.assertNotNull(id); + + final JSONObject opt = getOptionQueryService().getOptionById(Option.ID_C_BLOG_TITLE); + Assert.assertEquals(opt.getInt(Option.OPTION_VALUE), 0L); + } + + /** + * Update. + * + * @throws Exception exception + */ + @Test + public void update() throws Exception { + final OptionMgmtService optionMgmtService = getOptionMgmtService(); + + JSONObject option = new JSONObject(); + option.put(Keys.OBJECT_ID, Option.ID_C_BLOG_TITLE); + option.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + option.put(Option.OPTION_VALUE, 0L); + + final String id = optionMgmtService.addOrUpdateOption(option); // Add + //System.out.println(id); + Assert.assertNotNull(id); + + option = new JSONObject(); + option.put(Keys.OBJECT_ID, Option.ID_C_BLOG_TITLE); + option.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + option.put(Option.OPTION_VALUE, 1L); + + optionMgmtService.addOrUpdateOption(option); // Update + + final JSONObject opt = getOptionQueryService().getOptionById(Option.ID_C_BLOG_TITLE); + Assert.assertEquals(opt.getInt(Option.OPTION_VALUE), 1L); + } + + /** + * Remove. + * + * @throws Exception exception + */ + @Test + public void remove() throws Exception { + final OptionMgmtService optionMgmtService = getOptionMgmtService(); + + final JSONObject option = new JSONObject(); + option.put(Keys.OBJECT_ID, Option.ID_C_BLOG_TITLE); + option.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_PREFERENCE); + option.put(Option.OPTION_VALUE, 0L); + + final String id = optionMgmtService.addOrUpdateOption(option); + Assert.assertNotNull(id); + + optionMgmtService.removeOption(id); + + final JSONObject opt = getOptionQueryService().getOptionById(id); + Assert.assertNull(opt); + } +} diff --git a/src/test/java/org/b3log/solo/service/OptionQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/OptionQueryServiceTestCase.java new file mode 100644 index 00000000..f5526dd3 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/OptionQueryServiceTestCase.java @@ -0,0 +1,58 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link OptionQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.2, Jan 29, 2019 + * @since 0.6.0 + */ +@Test(suiteName = "service") +public class OptionQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Get Preference. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getPreference() throws Exception { + final OptionQueryService optionQueryService = getOptionQueryService(); + final JSONObject preference = optionQueryService.getPreference(); + + Assert.assertEquals(preference.getString(Option.ID_C_BLOG_TITLE), "Solo 的个人博客"); + } +} diff --git a/src/test/java/org/b3log/solo/service/PageMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/PageMgmtServiceTestCase.java new file mode 100644 index 00000000..9b194410 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/PageMgmtServiceTestCase.java @@ -0,0 +1,161 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Latkes; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Page; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link PageMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.3, Apr 19, 2019 + */ +@Test(suiteName = "service") +public class PageMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add Page. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addPage() throws Exception { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p1"); + page.put(Page.PAGE_TITLE, "page1 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + final String pageId = pageMgmtService.addPage(requestJSONObject); + + Assert.assertNotNull(pageId); + } + + /** + * Remove Page. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removePage() throws Exception { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p2"); + page.put(Page.PAGE_TITLE, "page2 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + final String pageId = pageMgmtService.addPage(requestJSONObject); + Assert.assertNotNull(pageId); + + final PageQueryService pageQueryService = getPageQueryService(); + JSONObject result = pageQueryService.getPage(pageId); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(Page.PAGE).getString(Page.PAGE_TITLE), "page2 title"); + + pageMgmtService.removePage(pageId); + + result = pageQueryService.getPage(pageId); + Assert.assertNull(result); + } + + /** + * Update Page. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updatePage() throws Exception { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p3"); + page.put(Page.PAGE_TITLE, "page3 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + final String pageId = pageMgmtService.addPage(requestJSONObject); + Assert.assertNotNull(pageId); + + final PageQueryService pageQueryService = getPageQueryService(); + JSONObject result = pageQueryService.getPage(pageId); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(Page.PAGE).getString(Page.PAGE_TITLE), "page3 title"); + + page.put(Page.PAGE_TITLE, "updated page3 title"); + pageMgmtService.updatePage(requestJSONObject); + + result = pageQueryService.getPage(pageId); + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONObject(Page.PAGE).getString(Page.PAGE_TITLE), "updated page3 title"); + } + + /** + * Change Order. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addPage") + public void changeOrder() throws Exception { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p4"); + page.put(Page.PAGE_TITLE, "page4 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + final String pageId = pageMgmtService.addPage(requestJSONObject); + Assert.assertNotNull(pageId); + + final int oldOrder = page.getInt(Page.PAGE_ORDER); + pageMgmtService.changeOrder(pageId, "up"); + + final JSONObject result = getPageQueryService().getPage(pageId); + Assert.assertNotNull(result); + Assert.assertTrue(oldOrder > result.getJSONObject(Page.PAGE).getInt(Page.PAGE_ORDER)); + } +} diff --git a/src/test/java/org/b3log/solo/service/PageQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/PageQueryServiceTestCase.java new file mode 100644 index 00000000..fee7e1fb --- /dev/null +++ b/src/test/java/org/b3log/solo/service/PageQueryServiceTestCase.java @@ -0,0 +1,84 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.latke.Latkes; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Page; +import org.b3log.solo.util.Solos; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link PageQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.3, Apr 19, 2019 + */ +@Test(suiteName = "service") +public class PageQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Add Page. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void addPage() throws Exception { + final PageMgmtService pageMgmtService = getPageMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + final JSONObject page = new JSONObject(); + requestJSONObject.put(Page.PAGE, page); + + page.put(Page.PAGE_PERMALINK, Latkes.getServePath() + "/p1"); + page.put(Page.PAGE_TITLE, "page1 title"); + page.put(Page.PAGE_OPEN_TARGET, "_self"); + + final String pageId = pageMgmtService.addPage(requestJSONObject); + + Assert.assertNotNull(pageId); + } + + /** + * Get Pages. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addPage") + public void getPages() throws Exception { + final PageQueryService pageQueryService = getPageQueryService(); + + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/10/20"); + final JSONObject result = pageQueryService.getPages(paginationRequest); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getJSONArray(Page.PAGES).length(), 1); + } +} diff --git a/src/test/java/org/b3log/solo/service/PermalinkQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/PermalinkQueryServiceTestCase.java new file mode 100644 index 00000000..59ced0b3 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/PermalinkQueryServiceTestCase.java @@ -0,0 +1,45 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link org.b3log.solo.service.PermalinkQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Apr 19, 2019 + * @since 0.6.1 + */ +public final class PermalinkQueryServiceTestCase { + + /** + * Test method for {@linkplain PermalinkQueryService#matchDefaultArticlePermalinkFormat(java.lang.String)}. + */ + @Test + public void matchDefaultArticlePermalinkFormat() { + Assert.assertTrue(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/articles/1986/08/25/1234567890.html")); + Assert.assertFalse(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/articles/1986/0/25/1234567890.html")); + Assert.assertFalse(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/articles/1.html")); + Assert.assertFalse(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/articles/1986/08/25/a.html")); + Assert.assertFalse(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/articles/1986/aa/25/1234567890.html")); + Assert.assertFalse(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/1986/aa/25/1234567890.html")); + Assert.assertFalse(PermalinkQueryService.matchDefaultArticlePermalinkFormat("/articles/1986/08/25/1234567890html")); + } +} diff --git a/src/test/java/org/b3log/solo/service/PreferenceMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/PreferenceMgmtServiceTestCase.java new file mode 100644 index 00000000..a2b8cc0f --- /dev/null +++ b/src/test/java/org/b3log/solo/service/PreferenceMgmtServiceTestCase.java @@ -0,0 +1,64 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Option; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * {@link PreferenceMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.4, May 29, 2018 + */ +@Test(suiteName = "service") +public class PreferenceMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Update Preference. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void updatePreference() throws Exception { + final PreferenceMgmtService preferenceMgmtService = getPreferenceMgmtService(); + final OptionQueryService optionQueryService = getOptionQueryService(); + JSONObject preference = optionQueryService.getPreference(); + + Assert.assertEquals(preference.getString(Option.ID_C_BLOG_TITLE), "Solo 的个人博客"); + + preference.put(Option.ID_C_BLOG_TITLE, "updated blog title"); + preferenceMgmtService.updatePreference(preference); + + preference = optionQueryService.getPreference(); + Assert.assertEquals(preference.getString(Option.ID_C_BLOG_TITLE), "updated blog title"); + } +} diff --git a/src/test/java/org/b3log/solo/service/TagMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/TagMgmtServiceTestCase.java new file mode 100644 index 00000000..f5d48363 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/TagMgmtServiceTestCase.java @@ -0,0 +1,53 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.testng.annotations.Test; + +/** + * {@link TagMgmtService} test case. + * + * @author Liang Ding + * @version 1.0.0.1, Sep 11, 2012 + */ +@Test(suiteName = "service") +public class TagMgmtServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Remove Unused Tags. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void removeUnusedTags() throws Exception { + final TagMgmtService tagMgmtService = getTagMgmtService(); + + tagMgmtService.removeUnusedTags(); + } +} diff --git a/src/test/java/org/b3log/solo/service/TagQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/TagQueryServiceTestCase.java new file mode 100644 index 00000000..acb3ee6d --- /dev/null +++ b/src/test/java/org/b3log/solo/service/TagQueryServiceTestCase.java @@ -0,0 +1,79 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.model.Tag; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link TagQueryService} test case. + * + * @author Liang Ding + * @version 1.0.0.3, Nov 9, 2016 + */ +@Test(suiteName = "service") +public class TagQueryServiceTestCase extends AbstractTestCase { + + /** + * Init. + * + * @throws Exception exception + */ + @Test + public void init() throws Exception { + super.init(); + } + + /** + * Get Tags. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getTags() throws Exception { + final TagQueryService tagQueryService = getTagQueryService(); + + final List tags = tagQueryService.getTags(); + Assert.assertNotNull(tags); + Assert.assertEquals(tags.size(), 1); + Assert.assertEquals(tags.get(0).getString(Tag.TAG_TITLE), "Solo"); + } + + /** + * Get Tag By Title. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "init") + public void getTagByTitle() throws Exception { + final TagQueryService tagQueryService = getTagQueryService(); + + final JSONObject result = tagQueryService.getTagByTitle("Solo"); + Assert.assertNotNull(result); + + final JSONObject tag = result.getJSONObject(Tag.TAG); + Assert.assertNotNull(tag); + Assert.assertEquals(tag.getString(Tag.TAG_TITLE), "Solo"); + + } +} diff --git a/src/test/java/org/b3log/solo/service/UserMgmtServiceTestCase.java b/src/test/java/org/b3log/solo/service/UserMgmtServiceTestCase.java new file mode 100644 index 00000000..bf92d8f2 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/UserMgmtServiceTestCase.java @@ -0,0 +1,131 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import junit.framework.Assert; +import org.b3log.latke.Keys; +import org.b3log.latke.model.Role; +import org.b3log.latke.model.User; +import org.b3log.latke.service.ServiceException; +import org.b3log.solo.AbstractTestCase; +import org.json.JSONObject; +import org.testng.annotations.Test; + +/** + * {@link UserMgmtService} test case. + * + * @author Liang Ding + * @author nanolikeyou + * @version 1.0.0.5, Aug 2, 2018 + */ +@Test(suiteName = "service") +public class UserMgmtServiceTestCase extends AbstractTestCase { + + /** + * Add User. + * + * @throws Exception exception + */ + @Test + public void addUser() throws Exception { + final UserMgmtService userMgmtService = getUserMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + + requestJSONObject.put(User.USER_NAME, "user1name"); + + final String id = userMgmtService.addUser(requestJSONObject); + Assert.assertNotNull(id); + } + + /** + * Update User. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addUser") + public void updateUser() throws Exception { + final UserMgmtService userMgmtService = getUserMgmtService(); + + JSONObject requestJSONObject = new JSONObject(); + requestJSONObject.put(User.USER_NAME, "user2name"); + requestJSONObject.put(User.USER_ROLE, Role.ADMIN_ROLE); + + final String id = userMgmtService.addUser(requestJSONObject); + Assert.assertNotNull(id); + + requestJSONObject.put(Keys.OBJECT_ID, id); + requestJSONObject.put(User.USER_NAME, "user2newname"); + + userMgmtService.updateUser(requestJSONObject); + + Assert.assertEquals(getUserQueryService().getUser(id).getJSONObject( + User.USER).getString(User.USER_NAME), "user2newname"); + } + + /** + * Valid User. + * + * @throws Exception exception + */ + @Test + public void validUser() throws Exception { + final UserMgmtService userMgmtService = getUserMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + requestJSONObject.put(User.USER_NAME, "user1 name"); + + try { + final String id = userMgmtService.addUser(requestJSONObject); + } catch (Exception e) { + Assert.assertTrue(e instanceof ServiceException); + } + } + + /** + * Valid XSS username. + * + * @throws Exception exception + */ + @Test(expectedExceptions = ServiceException.class) + public void XSSUser() throws Exception { + final UserMgmtService userMgmtService = getUserMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + requestJSONObject.put(User.USER_NAME, ""); + + final String id = userMgmtService.addUser(requestJSONObject); + } + + /** + * Remove User. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addUser") + public void removeUser() throws Exception { + final UserMgmtService userMgmtService = getUserMgmtService(); + + final JSONObject user = getUserQueryService().getUserByName("user1name"); + Assert.assertNotNull(user); + + userMgmtService.removeUser(user.getString(Keys.OBJECT_ID)); + + Assert.assertNull(getUserQueryService().getUserByName("user1name")); + } +} diff --git a/src/test/java/org/b3log/solo/service/UserQueryServiceTestCase.java b/src/test/java/org/b3log/solo/service/UserQueryServiceTestCase.java new file mode 100644 index 00000000..bb3e64c8 --- /dev/null +++ b/src/test/java/org/b3log/solo/service/UserQueryServiceTestCase.java @@ -0,0 +1,112 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.service; + +import junit.framework.Assert; +import org.b3log.latke.model.User; +import org.b3log.solo.AbstractTestCase; +import org.b3log.solo.util.Solos; +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.annotations.Test; + +/** + * {@link UserQueryService} test case. + * + * @author Liang Ding + * @author nanolikeyou + * @version 1.0.0.3, Feb 11, 2019 + */ +@Test(suiteName = "service") +public class UserQueryServiceTestCase extends AbstractTestCase { + + /** + * Add User. + * + * @throws Exception exception + */ + @Test + public void addUser() throws Exception { + final UserMgmtService userMgmtService = getUserMgmtService(); + + final JSONObject requestJSONObject = new JSONObject(); + + requestJSONObject.put(User.USER_NAME, "user1name"); + + final String id = userMgmtService.addUser(requestJSONObject); + Assert.assertNotNull(id); + + final UserQueryService userQueryService = getUserQueryService(); + Assert.assertNotNull(userQueryService.getUser(id)); + } + + /** + * Get User. + */ + @Test(dependsOnMethods = "addUser") + public void getUser() { + final UserQueryService userQueryService = getUserQueryService(); + Assert.assertNull(userQueryService.getUser("not found")); + } + + /** + * Get User By Name. + */ + @Test(dependsOnMethods = "addUser") + public void getUserByName() { + final UserQueryService userQueryService = getUserQueryService(); + + final JSONObject user = userQueryService.getUserByName("user1name"); + Assert.assertNotNull(user); + } + + /** + * Get Users. + * + * @throws Exception exception + */ + @Test(dependsOnMethods = "addUser") + public void getUsers() throws Exception { + final UserQueryService userQueryService = getUserQueryService(); + + final JSONObject paginationRequest = Solos.buildPaginationRequest("1/20/10"); + final JSONObject result = userQueryService.getUsers(paginationRequest); + final JSONArray users = result.getJSONArray(User.USERS); + Assert.assertEquals(1, users.length()); + } + + /** + * Get Login URL. + */ + public void getLoginURL() { + final UserQueryService userQueryService = getUserQueryService(); + final String loginURL = userQueryService.getLoginURL("redirectURL"); + + Assert.assertEquals(loginURL, "/start?referer=http%3A%2F%2Flocalhost%3A8080redirectURL"); + } + + /** + * Get Logout URL. + */ + public void getLogoutURL() { + final UserQueryService userQueryService = getUserQueryService(); + final String logoutURL = userQueryService.getLogoutURL(); + + Assert.assertEquals(logoutURL, "/logout?referer=http%3A%2F%2Flocalhost%3A8080"); + } +} diff --git a/src/test/java/org/b3log/solo/util/ImagesTestCase.java b/src/test/java/org/b3log/solo/util/ImagesTestCase.java new file mode 100644 index 00000000..7e3879f4 --- /dev/null +++ b/src/test/java/org/b3log/solo/util/ImagesTestCase.java @@ -0,0 +1,51 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * {@link org.b3log.solo.util.Images} test case. + * + * @author Liang Ding + * @version 1.0.0.0, Feb 14, 2018 + * @since 2.7.0 + */ +public final class ImagesTestCase { + + /** + * Test method for {@linkplain Images#randImage()}. + */ + @Test + public void randImage() { + final String url = Images.randImage(); + Assert.assertEquals(url.length(), "https://img.hacpai.com/bing/20171104.jpg".length()); + } + + /** + * Test method for {@linkplain Images#randomImages(int)}. + */ + @Test + public void randImages() { + final List urls = Images.randomImages(10); + Assert.assertEquals(urls.size(), 10); + } +} diff --git a/src/test/java/org/b3log/solo/util/MarkdownsTestCase.java b/src/test/java/org/b3log/solo/util/MarkdownsTestCase.java new file mode 100644 index 00000000..fdd4bb6d --- /dev/null +++ b/src/test/java/org/b3log/solo/util/MarkdownsTestCase.java @@ -0,0 +1,59 @@ +/* + * Solo - A small and beautiful blogging system written in Java. + * Copyright (c) 2010-present, b3log.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.b3log.solo.util; + +import org.b3log.latke.Latkes; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Locale; + +/** + * {@link org.b3log.solo.util.Markdowns} test case. + * + * @author Liang Ding + * @version 1.0.1.5, Feb 11, 2019 + * @since 0.4.5 + */ +public final class MarkdownsTestCase { + + @BeforeClass + public void beforeClass() { + Latkes.init(); + Latkes.setLocale(Locale.SIMPLIFIED_CHINESE); + } + + /** + * Test method for {@linkplain Markdowns#toHTML(java.lang.String)}. + * + * @throws Exception exception + */ + @Test + public void toHTML() throws Exception { + String markdownText = ""; + String html = Markdowns.toHTML(markdownText); + + Assert.assertEquals(html, ""); + + markdownText = "Solo Markdown"; + html = Markdowns.toHTML(markdownText); + + Assert.assertEquals(html, "

    Solo Markdown

    "); + } +} diff --git a/src/test/resources/latke.properties b/src/test/resources/latke.properties new file mode 100644 index 00000000..42252c79 --- /dev/null +++ b/src/test/resources/latke.properties @@ -0,0 +1,39 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: B3log Latke configurations for test. +# Version: 1.0.0.1, Feb 22, 2019 +# Author: Liang Ding +# + +#### Server #### +# Browser visit protocol +serverScheme=http +# Browser visit domain name +serverHost=localhost +# Browser visit port, 80 as usual, THIS IS NOT SERVER LISTEN PORT! +serverPort=8080 + +#### Cookie #### +cookieName=solo +cookieSecret=Beyond + +#### Runtime Mode #### +#runtimeMode=DEVELOPMENT +runtimeMode=PRODUCTION diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 00000000..897a3c80 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,44 @@ +# +# Solo - A small and beautiful blogging system written in Java. +# Copyright (c) 2010-present, b3log.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# +# Description: Solo logging configurations for test. +# Version: 1.0.0.0, Mar 3, 2019 +# Author: Liang Ding +# + +log4j.rootLogger=INFO,stdout + +log4j.logger.org.b3log.solo=INFO +log4j.logger.org.b3log.latke=WARN +log4j.logger.org.b3log.latke.util.freemarker.Templates=ERROR +log4j.logger.org.b3log.latke.repository.jdbc.util=WARN + +log4j.logger.org.eclipse.jetty=WARN +log4j.logger.freemarker=WARN +log4j.logger.com.mchange=WARN +log4j.logger.com.alibaba=WARN + +# Print only messages of level ERROR or above in the package noModule. +log4j.logger.noModule=ERROR + +# Console appender +log4j.appender.stdout.Encoding=UTF-8 +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%-5p]-[%d{yyyy-MM-dd HH:mm:ss}]-[%c:%L]: %m%n